v3.0.0 — Foundation release (10 features, all 5 v2.1 debts cleared)#80
Conversation
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.4 to 4.2.6. - [Release notes](https://github.com/socketio/socket.io/releases) - [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.4...socket.io-parser@4.2.6) --- updated-dependencies: - dependency-name: socket.io-parser dependency-version: 4.2.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Add CHANGELOG.md, ROADMAP.md, and versioning directory to formalize project documentation. Update CLAUDE.md with mandatory documentation location conventions and path standards to ensure all technical docs are centralized under the docs/ directory.
…/socket.io-parser-4.2.6 chore(deps)(deps): bump socket.io-parser from 4.2.4 to 4.2.6
Replace the in-memory deployment queue with a robust BullMQ implementation using Redis to ensure deployment jobs survive process restarts. - Integrate BullMQ and ioredis for persistent job management - Add Bull Board admin UI for queue monitoring (protected by Admin role) - Implement `QueueReadyMiddleware` to return 503 when Redis is unavailable - Add database migrations to support `QueueJobId` correlation - Implement one-shot migration to re-enqueue pending v2.1 deployments - Update `DeploymentService` to use asynchronous queue enqueuing - Add graceful shutdown logic to stop workers before closing database connections - Update Jest configuration with improved test discovery and coverage gating
Implement the `EnvironmentVariable` model and corresponding database migration to support encrypted per-project environment variable storage. - Add `EnvironmentVariable` model and associations with `Project` - Add migration `009_create_environment_variables` - Register new migration in `MigrationRunner`
Introduces support for project-level environment variables and optimizes deployment performance using a per-project bare git cache. - Add `EnvironmentVariable` controller, service, and routes to manage project secrets. - Implement `RedactSecrets` in `LogFormatter` to prevent secret leakage in deployment logs. - Implement a bare-clone git cache mechanism in `DeploymentService` to speed up repository preparation. - Add automatic cleanup of the git cache when a project is deleted. - Update `PipelineService` and `ShellSession` to inject environment variables into deployment processes. - Add migration documentation for v2 to v3 upgrade path.
Implements functionality to download deployment logs as text/plain attachments and significantly increases test coverage across services and utilities. - Add `DownloadDeploymentLog` to `DeploymentController` for log retrieval - Implement `ResolveLogFilePath` in `DeploymentService` to locate log files - Add `GET /api/deployments/:id/log/download` route - Update `QueueService` to use prefixed job IDs for BullMQ compatibility - Add comprehensive integration and unit tests for core services - Establish baseline code coverage thresholds in `jest.config.js` - Add `socket.io-client` to dependencies for testing purposes
Introduce a new notification architecture supporting providers, channels, and project-specific event subscriptions. This implementation includes support for Discord, Slack, and Email dispatchers. - Add `NotificationProvider`, `NotificationChannel`, and `ProjectNotificationSubscription` models and migrations. - Implement new controllers and services for managing notifications. - Refactor `NotificationService` to support both legacy configuration-based notifications and the new event-driven subscription model. - Add `ENotificationEvent` and `ENotificationProviderType` enums. - Extend `ETriggerType` and `EAuditAction` to include rollback events.
Implement `GetStatus` in `MigrationRunner` to allow checking the execution state of all registered migrations. This supports the `migrate:status` CLI command. Also, clean up comments in `.env.example`.
Add new CLI scripts for running, undoing, and checking migration status. Update `package.json` with corresponding npm scripts to expose these functionalities. Refactor `MigrationRunner` to use explicit `QueryTypes` when fetching migration history.
Update environment variable validation to provide more descriptive error messages for POSIX pattern mismatches. Refactor migration history retrieval to use explicit SELECT query types, preventing driver-specific errors when handling query results.
Introduce the workspace entity to allow grouping projects. This includes new database migrations, models, services, and API routes for workspace CRUD operations, as well as an endpoint to assign projects to workspaces. - Add `Workspace` model and `WorkspaceService` - Add `WorkspaceRoutes` and `WorkspaceController` - Update `Project` model with `WorkspaceId` foreign key - Add migration `016_create_workspaces` - Implement `PATCH /api/projects/:projectId/workspace` for assignment
…eware
Implements stricter database schema management by disabling automatic
column mutation via `sync({ alter: true })`. Schema changes must now
be handled through migrations to prevent silent mutations. Added a
`DB_FORCE_SYNC_ALTER` environment flag as a development-only escape
hatch.
Refines SQL injection prevention in `SecurityMiddleware` by anchoring
comment patterns to prevent false positives on hex colors and Slack
channel names. Expanded the list of excluded fields to include new
v3.0 workspace and notification configuration properties.
Implement the rollback feature (F-007) allowing users to revert failed deployments to the last successful commit. This includes a new RollbackService, deployment controller endpoints, and socket event emissions for real-time UI updates. Additionally, this commit ratchets up the global Jest coverage thresholds and adds extensive unit and integration tests for auth, notifications, rollback, and audit logging. - add RollbackService and DeploymentController.Rollback - add deployment:rollback-queued socket event - add Conflict and UnprocessableEntity helpers to ResponseHelper - increase Jest coverage thresholds (lines: 30%, statements: 30%, functions: 25%, branches: 18%) - add integration tests for Auth, Notifications, and Rollback - add unit tests for AuditLog, AutoRecovery, and Notification dispatchers
Introduce project template functionality (F-008) including a new ProjectTemplate model, service, controller, and migration to seed five built-in templates (Node.js, React, Next.js, Static, Astro). This commit also marks the official v3.0.0 release by updating documentation, finalizing the testing foundation with increased coverage thresholds, and adding integration tests for core entities. - add ProjectTemplate model, service, and routes - add migration 017 for project templates - increase Jest global coverage thresholds to meet GA requirements - add integration tests for Deployments, Projects, and Users - update CHANGELOG, CLAUDE, and migration documentation for v3.0 release
Update sequelize to version 6.37.8 and introduce overrides for uuid, lodash, validator, and dottie to ensure consistent versions across the dependency tree.
Add migration 020 to remove deprecated per-user notification fields (EmailNotifications, DiscordWebhookUrl, SlackWebhookUrl, and various NotifyOn* flags) from the UserSettings model. This cleanup aligns the database schema with the new centralized NotificationProvider/Channel/Subscription architecture (F-006) and removes unused columns that were not integrated into the deployment fan-out path.
Use sequelize.query with QueryTypes.RAW instead of queryInterface.removeColumn to bypass a TypeError in the mariadb driver when column-mutation statements return an array. This also includes updated column definitions for the down migration to ensure schema consistency during rollbacks.
Update hashed asset filenames in `public/index.html` and replace old vendor chunks with new versions to reflect recent build changes.
Updates README + ROADMAP to reflect the v3.0 Foundation release: - README version badge 2.1.0 → 3.0.0 - Replace "coming soon" markers for features that shipped (Slack/Email, persistent queue, rollback UI, project templates, workspaces, encrypted env vars, testing foundation, CI pipeline) - ROADMAP table row for v3.0 → Released; per-feature checkmarks on all 10 - v3.1 promoted from Planned → Active Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Your plan currently allows 1 review/hour. Refill in 43 minutes and 35 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughv3.0 upgrades: add a persistent BullMQ+Redis queue, Redis lifecycle and Redis-ready middleware, notification provider/channel/subscription model and dispatchers (Discord/Slack/Email), encrypted per-project environment variables and injection, workspaces/project templates, rollback API, many migrations/models/services/controllers/routes, extensive tests, docs, and CI/test tooling changes. Changesv3.0 cohesive upgrade
Sequence diagram(s)
Estimated code review effort:
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 45e337dd0c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } catch (err) { | ||
| const msg = (err as Error).message; | ||
| failures.push({ Id: row.Id, error: msg }); | ||
| console.error(`❌ Migration 999: failed to enqueue deployment ${row.Id}: ${msg}`); | ||
| } |
There was a problem hiding this comment.
Fail migration when enqueueing pending rows fails
This migration catches enqueue errors per row and continues, then exits successfully, so MigrationRunner will record 999_migrate_pending_deployments as executed even when Redis is down or enqueueing fails. In that case, legacy pending/queued deployments remain unmigrated permanently unless operators manually reset migration state, which breaks the one-shot queue migration guarantee.
Useful? React with 👍 / 👎.
| const jobId = await this.QueueService.Enqueue( | ||
| deployment.Id, | ||
| JsonProject.Id, | ||
| async () => await this.ExecuteDeployment(deployment.Id), | ||
| params.ManualTrigger ? 10 : 0 // Higher priority for manual deployments | ||
| ); | ||
| deployment.QueueJobId = jobId; | ||
| await deployment.save(); |
There was a problem hiding this comment.
Roll back deployment creation when enqueue fails
The service creates a Deployments row with Status='queued' before enqueueing, but if QueueService.Enqueue throws (e.g., Redis race/outage after readiness check), the catch rethrows without compensating changes. This leaves an orphaned queued deployment with no QueueJobId that will never run, causing stuck state in queue/status views and requiring manual cleanup.
Useful? React with 👍 / 👎.
| const jobId = await QueueService.GetInstance().Enqueue( | ||
| rollbackDeployment.Id, | ||
| rollbackDeployment.ProjectId, | ||
| 20 | ||
| ); | ||
| rollbackDeployment.QueueJobId = jobId; | ||
| await rollbackDeployment.save(); |
There was a problem hiding this comment.
Revert rollback deployment row on enqueue failure
Rollback creates a new deployment row before enqueueing; if enqueue fails, the method throws and returns an API error but leaves the newly created rollback deployment in queued state with no job id. This produces false rollback records that cannot execute and pollutes deployment history unless cleaned manually.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
package.json (1)
3-3:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPackage version is not aligned with the v3.0.0 release objective.
"version": "2.1.2"should be updated to the intended v3 release version before tagging/publishing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@package.json` at line 3, Update the package.json "version" field from "2.1.2" to the target release "3.0.0" so the package matches the v3.0.0 release; after changing the "version" key, regenerate/update any lockfiles (e.g., package-lock.json or yarn.lock) and bump references/badges/docs that reference the old version string to keep metadata consistent.jest.config.js (1)
9-34:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAlign
ts-jestwith Jest 30.2.0 (ts-jest29.4.6 may be untested for this exact Jest patch)This repo uses
jest30.2.0 withts-jest29.4.6. ts-jest only added Jest 30 support starting in 29.4.0, and ts-jest warns that untested Jest 30 versions aren’t covered for bug reports—so CI/test transform can break for this exact pair.
- Pin
jestto a ts-jest-tested range or upgradets-jestto a version that explicitly supports your Jest 30.2.x setup.- Ensure the “not tested with ts-jest” warning (if emitted) is addressed before relying on CI results.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jest.config.js` around lines 9 - 34, The project currently uses Jest 30.2.0 with ts-jest 29.4.6 (configured via the preset 'ts-jest' and the transform entry '^.+\\.ts$': ['ts-jest', { tsconfig: 'tsconfig.json' }]), which may be an untested combination; update dependency alignment by either pinning jest to a ts-jest-tested range (downgrade jest in package.json) or upgrading ts-jest to a version that explicitly supports Jest 30.2.x, then run the test suite to confirm the "not tested with ts-jest" warning is cleared and CI transforms succeed..env.example (1)
1-99:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
.env.exampleviolates the current.env*repository policy.This file is being committed under a rule that disallows
.env*files in the repository.As per coding guidelines "
.env*: Never commit.envfiles or sensitive data to the repository".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.env.example around lines 1 - 99, The .env.example file contains real-looking secrets and violates the repo policy forbidding committed .env* files; remove this file from the commit (unstage/delete .env.example), add a sanitized template (e.g. env.example.sample) that contains only placeholder values (replace JWT_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY, DB_PASSWORD, DB_ROOT_PASSWORD, SESSION_SECRET, DEFAULT_ADMIN_PASSWORD, SMTP_PASSWORD, REDIS_PASSWORD, GITHUB_WEBHOOK_SECRET, DISCORD/SLACK/TELEGRAM tokens, etc. with non-sensitive placeholders) and ensure the repo ignores any .env* by adding/updating .gitignore to include .env* so actual env files are never committed.
🟡 Minor comments (14)
CLAUDE.md-67-72 (1)
67-72:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove policy exceptions that contradict the repository docs-location rule.
This new exception list allows Markdown files outside
server/docs/, which conflicts with the enforced documentation policy and creates ambiguous standards.As per coding guidelines: "
**/*.md: Place all documentation files exclusively underserver/docs/- no.mddocumentation files in root,client/, or other directories".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CLAUDE.md` around lines 67 - 72, The "الاستثناءات الوحيدة" section in CLAUDE.md currently exempts `CLAUDE.md` (and other Markdown files) from the repository docs-location rule, which conflicts with the enforced policy requiring all `**/*.md` docs to live under server/docs/; to fix it, remove or modify the exception lines that state "`CLAUDE.md` يبقى في `server/CLAUDE.md`" and any related bullets so the "الاستثناءات الوحيدة" section no longer permits Markdown files outside server/docs/, and update CLAUDE.md to reference the global documentation rule instead (locate and edit the "الاستثناءات الوحيدة" header in CLAUDE.md to align its text with the repository's docs-location policy).__tests__/unit/Services/QueueService.test.ts-44-44 (1)
44-44:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix the no-op shared-client touch.
Line 44 currently references
getTestRediswithout invoking it, so the intended side-effect does not happen.Suggested patch
- void getTestRedis; + void getTestRedis();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@__tests__/unit/Services/QueueService.test.ts` at line 44, The test currently references getTestRedis as a no-op symbol (void getTestRedis;) so its side-effect never runs; replace that reference with an actual invocation — call getTestRedis() and await it if it returns a Promise (e.g., await getTestRedis() or void await getTestRedis()); ensure the invocation is placed where the side-effect was intended in QueueService.test.ts so the shared-client touch occurs.__tests__/unit/Services/QueueService.test.ts-127-129 (1)
127-129:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign slow-test job id expectation with the deterministic format.
Line 128 expects
'9999', but Line 74 in the same suite assertsEnqueuereturnsdep-<id>. This inconsistency can fail the opt-in slow test.Suggested patch
- expect(jobId).toBe('9999'); + expect(jobId).toBe('dep-9999');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@__tests__/unit/Services/QueueService.test.ts` around lines 127 - 129, The test is expecting the wrong job id format; update the assertion in QueueService.test.ts to match the deterministic "dep-<id>" format used elsewhere (e.g., change the expected value from '9999' to 'dep-9999' for the call to Enqueue), ensuring the test that calls Enqueue(9999, PROJECT_ID, 0) checks for 'dep-9999' so it aligns with the other assertion at Line 74 that Enqueue returns dep-<id>.docs/CHANGELOG.md-102-103 (1)
102-103:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix inconsistent migration guide path reference.
The text points to
server/docs/migration-v2-to-v3.md, but this PR introducesdocs/migration-v2-to-v3.md. Align the referenced path to prevent operator confusion.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/CHANGELOG.md` around lines 102 - 103, Update the inconsistent documentation path in CHANGELOG.md by replacing the incorrect reference string `server/docs/migration-v2-to-v3.md` with the correct `docs/migration-v2-to-v3.md` so the line referencing the migration guide (and the F-003 envVars migration helper SQL) points to the new file location.docs/ROADMAP.md-45-45 (1)
45-45:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd language tags to fenced code blocks to satisfy markdown linting.
Both fenced blocks are missing language identifiers (MD040), which will keep docs lint noisy.
Suggested fix
-``` +```text MAJOR.MINOR.PATCH | | | | | └─ Bug fixes فقط (v3.0.1, v3.0.2, ...) | └─ ميزات جديدة backward-compatible (v3.0 → v3.1) └─ كسر backward compatibility / إعادة هيكلة كبرى (v3 → v4)-
+text
2026:
├── May ──→ v2.1.x (current) - maintenance
...
└── Q3+ → v5.0 GA — Cloud Native (Multi-Region + Distributed + Plugin Marketplace)Also applies to: 72-72
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/ROADMAP.md` at line 45, Two fenced code blocks in ROADMAP.md are missing language identifiers (MD040); update the opening fences for the two blocks that contain the version diagram (the block starting with "MAJOR.MINOR.PATCH...") and the 2026 roadmap block (the block starting with "2026:") to include a language tag (e.g., change ``` to ```text) so markdown linting stops flagging them; locate the fences around those snippets and add the language token to the opening backticks.docs/migration-v2-to-v3.md-25-25 (1)
25-25:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix the docker-compose link path (likely broken).
From
docs/migration-v2-to-v3.md,../../docker-compose.ymltraverses too far up. Use the correct relative path from the docs directory.Suggested fix
-- **Redis 7+** reachable from the Deploy Center host. Docker Compose recipe - bundled — see [docker-compose.yml](../../docker-compose.yml). +- **Redis 7+** reachable from the Deploy Center host. Docker Compose recipe + bundled — see [docker-compose.yml](../docker-compose.yml).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/migration-v2-to-v3.md` at line 25, The markdown link "bundled — see [docker-compose.yml](../../docker-compose.yml)" points too far up; update the link target to the correct relative path from the docs file (replace "../../docker-compose.yml" with "../docker-compose.yml") so the "[docker-compose.yml]" link resolves to the repository root docker-compose file.docs/release-engineering.md-5-6 (1)
5-6:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix likely broken relative links in source references.
From
docs/release-engineering.md,../../.specify/...and../../specs/...likely resolve outside the repository root. These should probably be one level up (for example../.specify/...,../specs/...) based on actual repo layout.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/release-engineering.md` around lines 5 - 6, The relative links in docs/release-engineering.md use ../../ (e.g., ../../.specify/memory/constitution.md and ../../specs/001-v3-foundation/spec.md) which likely point outside the repo; update these references to the correct repo-relative paths (for example ../.specify/memory/constitution.md and ../specs/001-v3-foundation/spec.md) so they resolve inside the repository root, verify by opening the links locally or running the link-checker, and adjust any other nearby references using the same ../../ pattern.docs/versions/v3.1-remote-targets.md-54-54 (1)
54-54:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSpecify a language on the fenced API block.
The code fence starting at Line 54 is missing a language tag (MD040). Add one (
textis fine if it’s plain route listing).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/versions/v3.1-remote-targets.md` at line 54, The fenced code block in docs/versions/v3.1-remote-targets.md (the triple-backtick block around the route listing) is missing a language tag and triggers MD040; add a language identifier by changing the opening fence from ``` to ```text (or another appropriate language) so the Markdown linter recognizes the block type.docs/versions/v3.0-foundation.md-82-82 (1)
82-82:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd language identifiers to fenced code blocks to satisfy markdown lint.
These fenced blocks are missing language tags (MD040). Please annotate them (for example
text,bash,json,typescript) so markdownlint passes consistently.Also applies to: 97-97, 149-149, 212-212, 269-269, 393-393
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/versions/v3.0-foundation.md` at line 82, Several fenced code blocks in docs/versions/v3.0-foundation.md are missing language identifiers (triple backticks with no language) causing markdownlint MD040 failures; update each ``` block at the mentioned occurrences to include an appropriate language tag (e.g., ```text, ```bash, ```json, ```typescript) so the fences are annotated consistently and linting passes—search for the bare ``` tokens in that file and replace them with the correct annotated fences.docs/versions/v5.0-cloud-native.md-104-115 (1)
104-115:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd a language identifier to the fenced code block.
The code fence should be tagged (for example,
text) to satisfy markdown linting.Proposed fix
-``` +```text Current (v4.x): Single Node.js server → Redis → MySQL → SSH/SFTP to targets @@ -``` +```🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/versions/v5.0-cloud-native.md` around lines 104 - 115, Add a language identifier to the fenced code block that begins with the lines "Current (v4.x): Single Node.js server → Redis → MySQL → SSH/SFTP to targets" and the "v5.x:" architecture diagram; change the opening triple backticks to include a language token such as text (e.g., ```text) so the block is properly tagged for markdown linting while leaving the block contents unchanged.src/Controllers/NotificationChannelController.ts-59-62 (1)
59-62:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winValidate
providerIdquery before calling the service.
parseIntcan produceNaN, which currently gets passed through.Suggested fix
const providerId = req.query.providerId ? parseInt(String(req.query.providerId), 10) : undefined; + if (providerId !== undefined && (Number.isNaN(providerId) || providerId <= 0)) { + ResponseHelper.ValidationError(res, 'Invalid providerId'); + return; + } const items = await this.Service.List(providerId);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Controllers/NotificationChannelController.ts` around lines 59 - 62, The code extracts providerId from req.query and uses parseInt which can produce NaN and is then passed to this.Service.List; change the logic in NotificationChannelController so after computing providerId (from req.query.providerId via parseInt), validate it with Number.isFinite or Number.isInteger (or !Number.isNaN) and only pass a real number to this.Service.List, otherwise set providerId to undefined or return a 400 for invalid input; update the variable handling around providerId and the call to this.Service.List accordingly.src/Controllers/WorkspaceController.ts-35-43 (1)
35-43:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReject malformed route IDs in
parseId(WorkspaceController)
parseInt()accepts numeric prefixes (e.g.,42abc→42), so the current check can validate an invalid route ID and operate on the wrong workspace/project.- Handler properties
List/Create/Update/Delete/AssignProjectWorkspaceare PascalCase and conflict with the camelCase naming guideline.Suggested fix
function parseId(req: Request, res: Response, name = 'id'): number | null { const raw = req.params[name]; - const id = raw ? parseInt(raw, 10) : NaN; - if (Number.isNaN(id) || id <= 0) { + if (!raw || !/^[1-9]\d*$/.test(raw)) { ResponseHelper.ValidationError(res, `Invalid ${name}`); return null; } - return id; + return Number(raw); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Controllers/WorkspaceController.ts` around lines 35 - 43, The parseId function currently accepts strings like "42abc" because parseInt tolerates numeric prefixes; update parseId (function parseId) to validate req.params[name] against a strict digits-only pattern (e.g., /^\d+$/) before converting to a number and return a ValidationError if it fails, then parseInt/Number(...) and verify >0; also rename the exported handler properties List, Create, Update, Delete, AssignProjectWorkspace to camelCase (list, create, update, delete, assignProjectWorkspace) to follow the project's naming convention and update any references accordingly.src/Controllers/ProjectTemplateController.ts-41-47 (1)
41-47:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReject malformed
:idvalues (numeric prefixes) inparseId().
parseId()usesparseInt(req.params.id!, 10)and only checksNaN/<= 0, so inputs like"123abc"get treated as123and won’t hitResponseHelper.ValidationError(400). Validate the entire:idstring as a positive integer before converting.Suggested fix
function parseId(req: Request, res: Response): number | null { - const id = parseInt(req.params.id!, 10); - if (Number.isNaN(id) || id <= 0) { + const rawId = req.params.id; + if (!rawId || !/^[1-9]\d*$/.test(rawId)) { ResponseHelper.ValidationError(res, 'Invalid id'); return null; } - return id; + return Number(rawId); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Controllers/ProjectTemplateController.ts` around lines 41 - 47, parseId currently uses parseInt(req.params.id!, 10) which accepts inputs like "123abc"; change parseId to first validate the raw req.params.id string against a strict positive-integer pattern (e.g. /^[1-9]\d*$/) and call ResponseHelper.ValidationError(res, 'Invalid id') returning null if the string does not match, otherwise safely convert to number (parseInt or Number) and return it; update references to parseId to preserve behavior.src/Utils/ResponseHelper.ts-99-110 (1)
99-110:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse camelCase for
ResponseHelperstatic helper method names
src/Utils/ResponseHelper.tsdefinespublic static ServiceUnavailable,Conflict, andUnprocessableEntityin PascalCase, but the repo guideline for**/*.{ts,tsx}requires camelCase for function names. Rename these helpers (e.g.,serviceUnavailable,conflict,unprocessableEntity) and update call sites:
src/Middleware/QueueReadyMiddleware.ts(ResponseHelper.ServiceUnavailable(...))src/Controllers/ProjectTemplateController.ts(ResponseHelper.UnprocessableEntity(...))src/Controllers/DeploymentController.ts(ResponseHelper.Conflict(...),ResponseHelper.UnprocessableEntity(...))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Utils/ResponseHelper.ts` around lines 99 - 110, Three static helper methods on ResponseHelper are using PascalCase; rename ServiceUnavailable -> serviceUnavailable, Conflict -> conflict, and UnprocessableEntity -> unprocessableEntity in the ResponseHelper class (preserve method signatures and return types) and update all call sites to the new camelCase names: change ResponseHelper.ServiceUnavailable(...) in QueueReadyMiddleware to ResponseHelper.serviceUnavailable(...), ResponseHelper.UnprocessableEntity(...) in ProjectTemplateController to ResponseHelper.unprocessableEntity(...), and ResponseHelper.Conflict(...) / ResponseHelper.UnprocessableEntity(...) in DeploymentController to ResponseHelper.conflict(...) / ResponseHelper.unprocessableEntity(...); ensure any exports/typings remain valid after the rename.
🧹 Nitpick comments (14)
__tests__/integration/Auth.test.ts (1)
102-106: ⚡ Quick winAssert
refresh_tokenclearing on logout too.This flow only verifies
access_tokenis cleared. Please also assertrefresh_tokenis cleared so logout regression coverage includes both auth cookies.✅ Suggested assertion addition
const clears = logout.headers['set-cookie']; const cookieStr = Array.isArray(clears) ? clears.join(';') : String(clears); // Expect access_token and refresh_token to be cleared (Max-Age=0 or expires past). expect(cookieStr).toMatch(/access_token=;|access_token=.*(Max-Age=0|Expires=Thu, 01 Jan 1970)/i); + expect(cookieStr).toMatch(/refresh_token=;|refresh_token=.*(Max-Age=0|Expires=Thu, 01 Jan 1970)/i);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@__tests__/integration/Auth.test.ts` around lines 102 - 106, The logout test currently only asserts that access_token is cleared; extend it to also assert that refresh_token is cleared by inspecting the same cookie string (variables logout, clears, cookieStr) and adding an expectation mirroring the access_token check (matching refresh_token with either empty value or Max-Age=0/Expires past) so both auth cookies are validated on logout.__tests__/integration/Deployments.test.ts (1)
98-110: ⚡ Quick winAdd the retry happy-path integration case.
The suite currently only checks retry rejection on
Success. Adding aFailed -> retry -> 201case would lock in the core retry contract and queue handoff behavior.🧪 Suggested test addition
+ it('POST /api/deployments/:id/retry on Failed → 201 with new row', async () => { + if (!dbUp) return; + const admin = await makeUser({ Role: 'Admin' }); + const project = await makeProject({ CreatedBy: admin.Id }); + const failed = await makeDeployment({ + ProjectId: project.Id, + Status: EDeploymentStatus.Failed as never, + }); + + const res = await request(app) + .post(`/api/deployments/${failed.Id}/retry`) + .set(authHeader(admin)); + + expect(res.status).toBe(201); + expect(res.body.Data.Deployment.Id).toBeGreaterThan(failed.Id); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@__tests__/integration/Deployments.test.ts` around lines 98 - 110, Add a new integration test in __tests__/integration/Deployments.test.ts that creates an Admin user and a Project, then creates a deployment with Status set to EDeploymentStatus.Failed using makeDeployment, calls POST /api/deployments/${failed.Id}/retry with authHeader(admin), and assert the response status is 201; additionally verify the retry handoff by asserting the deployment was enqueued/updated (e.g., check the database record via the same model used elsewhere or assert the queue/job-mock was called) to ensure the retry path and queue handoff behavior are exercised.__tests__/integration/SocketLogStream.test.ts (1)
33-37: 🏗️ Heavy liftThis test bypasses the app’s socket wiring path.
Because it emits directly from an ad-hoc
IoServer, regressions inSocketService/server integration won’t be detected by this suite.Also applies to: 67-70
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@__tests__/integration/SocketLogStream.test.ts` around lines 33 - 37, The test is creating an ad-hoc IoServer (IoServer) and emitting directly, bypassing the app's socket wiring and not exercising SocketService/server integration; update the test to boot the actual app/socket wiring instead of new IoServer — for example, start or import the real server/socket initializer used in production (the code path that registers SocketService handlers) and obtain its httpServer/port to connect clients, then emit through that real server so SocketService integration is exercised; make the same change for the second occurrence around lines 67-70. Ensure you reference and use the app's socket initialization function or SocketService bootstrap rather than constructing a raw IoServer in the test.__tests__/integration/Rollback.test.ts (1)
74-198: ⚡ Quick winAdd the missing 403 authorization rollback case.
This suite validates rollback state rules well, but it does not currently assert that an unrelated developer is blocked from calling rollback (the access-control path).
🔐 Suggested test addition
+ it('403: unrelated developer cannot rollback another project deployment', async () => { + if (!dbUp) return; + const admin = await makeUser({ Role: 'Admin' }); + const developer = await makeUser({ Role: 'Developer' }); + const project = await makeProject({ CreatedBy: admin.Id }); + const failed = await makeDeployment({ + ProjectId: project.Id, + Status: EDeploymentStatus.Failed as never, + CommitHash: 'denied123', + }); + + const res = await request(app) + .post(`/api/deployments/${failed.Id}/rollback`) + .set(authHeader(developer)); + + expect(res.status).toBe(403); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@__tests__/integration/Rollback.test.ts` around lines 74 - 198, Add a new test case to assert a 403 when a non-admin developer attempts rollback: create a Developer user via makeUser({ Role: 'Developer' }), a project (owned by some other user or admin) and a failed deployment via makeDeployment(...) with EDeploymentStatus.Failed, then POST to /api/deployments/${failed.Id}/rollback with authHeader(developer) and expect res.status toBe(403); also assert no AuditLog entry exists for EAuditAction.DeploymentRolledBack and that QueueService.GetInstance().Enqueue was not called for the failed/new deployment ID. Use existing helpers (makeUser, makeProject, makeDeployment, authHeader, AuditLog, QueueService) and the same patterns as the other tests.__tests__/unit/Services/QueueService.test.ts (1)
19-19: ⚡ Quick winUse camelCase for the project id variable.
PROJECT_IDshould be camelCase to match repository TypeScript naming rules.As per coding guidelines: "Use camelCase for function names and variable names in TypeScript".Suggested patch
-const PROJECT_ID = 9001; +const projectId = 9001; @@ - const jobId = await QueueService.GetInstance().Enqueue(1, PROJECT_ID, 0); + const jobId = await QueueService.GetInstance().Enqueue(1, projectId, 0); @@ - await QueueService.GetInstance().Enqueue(2, PROJECT_ID, 0); - await QueueService.GetInstance().Enqueue(3, PROJECT_ID, 0); - const len = await QueueService.GetInstance().GetQueueLength(PROJECT_ID); + await QueueService.GetInstance().Enqueue(2, projectId, 0); + await QueueService.GetInstance().Enqueue(3, projectId, 0); + const len = await QueueService.GetInstance().GetQueueLength(projectId); @@ - const before = await QueueService.GetInstance().GetQueueLength(PROJECT_ID); - const cancelled = await QueueService.GetInstance().CancelPendingDeployments(PROJECT_ID); + const before = await QueueService.GetInstance().GetQueueLength(projectId); + const cancelled = await QueueService.GetInstance().CancelPendingDeployments(projectId); @@ - const jobId = await qs.Enqueue(9999, PROJECT_ID, 0); + const jobId = await qs.Enqueue(9999, projectId, 0);Also applies to: 73-74, 86-89, 94-95, 127-127
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@__tests__/unit/Services/QueueService.test.ts` at line 19, Rename the all-caps constant PROJECT_ID to camelCase (e.g., projectId) in __tests__/unit/Services/QueueService.test.ts and update every reference to it in the test (including where it's declared and used inside tests like any setup or assertions); ensure any other uppercase test constants mentioned in the file follow the same camelCase convention (the comment also flagged other occurrences around the file) and run the tests to confirm no broken references remain.src/Database/MigrationRunner.ts (1)
201-226: ⚡ Quick winKeep the new public API in camelCase.
Since this is new surface area, I’d avoid extending the PascalCase method pattern here.
getStatus()matches the repository’s TypeScript naming rule without forcing a broader cleanup in this file.As per coding guidelines,
**/*.{ts,tsx}: Use camelCase for function names and variable names in TypeScript.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Database/MigrationRunner.ts` around lines 201 - 226, The public method is named GetStatus (PascalCase) but should follow the repo camelCase convention; rename MigrationRunner.GetStatus to getStatus and update all internal references (including the static call sites and any tests) to use getStatus, keeping the implementation and return type identical and ensuring references to this.Migrations and EnsureMigrationsTable remain correct.src/Middleware/QueueReadyMiddleware.ts (1)
22-38: ⚡ Quick winRename the exported middleware to camelCase.
RequireQueueReadyadds a new PascalCase function name to the module API.requireQueueReadywould match the repo’s TypeScript naming convention.As per coding guidelines,
**/*.{ts,tsx}: Use camelCase for function names and variable names in TypeScript.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Middleware/QueueReadyMiddleware.ts` around lines 22 - 38, The exported middleware function is named in PascalCase (RequireQueueReady) but should be camelCase; rename the function to requireQueueReady and update the default export to export default requireQueueReady, keeping the internal logic (calls to QueueService.GetInstance().IsReady(), Logger.Warn, ResponseHelper.ServiceUnavailable, UNAVAILABLE_MESSAGE) unchanged; also search-and-replace any imports/usages of RequireQueueReady to requireQueueReady so consumers compile and tests pass.src/Controllers/WorkspaceController.ts (1)
54-162: ⚡ Quick winKeep the new handler names in camelCase.
The new controller surface uses PascalCase method names (
List,Create,AssignProjectWorkspace, etc.). Please align new TypeScript function names with the repo’s camelCase convention.As per coding guidelines,
**/*.{ts,tsx}: Use camelCase for function names and variable names in TypeScript.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Controllers/WorkspaceController.ts` around lines 54 - 162, The controller methods are using PascalCase; rename them to camelCase to match project conventions: change WorkspaceController.List -> list, Create -> create, Update -> update, Delete -> delete, and AssignProjectWorkspace -> assignProjectWorkspace (locations: the class WorkspaceController method definitions and any external usages such as route handlers or tests that call these methods). Update all references to these symbols (e.g., new WorkspaceController().List -> .list) and ensure exported default remains unchanged.src/Controllers/ProjectTemplateController.ts (1)
50-168: ⚡ Quick winUse camelCase for the new controller handlers.
The new public methods are introduced as
List,GetById,Create,Update, andDelete. Please keep new TypeScript function names in camelCase to match the repository convention.As per coding guidelines,
**/*.{ts,tsx}: Use camelCase for function names and variable names in TypeScript.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Controllers/ProjectTemplateController.ts` around lines 50 - 168, Rename the controller methods to camelCase to follow project conventions: change ProjectTemplateController.List -> list, GetById -> getById, Create -> create, Update -> update, and Delete -> delete; update all internal method definitions in the class (the async handlers currently named List, GetById, Create, Update, Delete) and ensure all external usages (route bindings, tests, or any callers that reference ProjectTemplateController methods) are updated to the new names so nothing breaks; keep all other logic (parseId usage, TemplateImmutableError handling, ResponseHelper and Logger calls, and the Service property) unchanged.src/Migrations/012_add_queue_job_id_to_deployments.ts (1)
10-13: ⚡ Quick winRename migration-scoped variables to camelCase.
TABLE,COLUMN, andINDEXviolate the project’s TypeScript naming rule for variables.As per coding guidelines
**/*.{ts,tsx}: Use camelCase for function names and variable names.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Migrations/012_add_queue_job_id_to_deployments.ts` around lines 10 - 13, Rename the migration-scoped constants TABLE, COLUMN, and INDEX to camelCase (e.g., table, column, index) and update every reference in this migration file (including inside up/down functions and any SQL/knex calls) to use the new names; ensure any created index or column names embedded in strings are updated to the new variable identifiers so the migration logic remains identical but variable names follow the project's camelCase rule.src/Migrations/999_migrate_pending_deployments.ts (1)
22-23: Gate merge on green CI for this migration-heavy release.Given the number of schema + queue-path changes, require full CI (typecheck/lint/tests/coverage) to pass before merge.
Based on learnings: Require successful completion of GitHub Actions CI pipeline (typecheck, lint, test, coverage gates) before merging code.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Migrations/999_migrate_pending_deployments.ts` around lines 22 - 23, Enable a "merge on green" CI gate for this release by configuring repository branch protection and documentation: in GitHub branch protection for the target branch require pull requests, enable "Require status checks to pass before merging" and add the workflow job names for typecheck, lint, test, and coverage (match the exact job names used by your GitHub Actions), enable "Require branches to be up to date before merging", and ensure administrators cannot bypass; also update CONTRIBUTING.md or the PR template referencing this migration file (999_migrate_pending_deployments.ts) and its up(queryInterface) change to state that merges require all CI checks (typecheck, lint, tests, coverage) to pass.src/Models/Project.ts (1)
160-167: ⚡ Quick winAdd an index for
WorkspaceIdto support workspace-scoped project queries.The new foreign key is nullable and will likely be used in filters/joins; indexing it avoids avoidable scans as
Projectsgrows.Proposed diff
indexes: [ { name: 'idx_projects_name', unique: true, fields: ['Name'], }, { name: 'idx_projects_is_active', fields: ['IsActive'], }, + { + name: 'idx_projects_workspace_id', + fields: ['WorkspaceId'], + }, ],🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Models/Project.ts` around lines 160 - 167, Add a DB index for the new nullable foreign key to avoid table scans: update the Project model (the WorkspaceId column definition) to include a model-level index on ['WorkspaceId'] (e.g., add an indexes array to the Project model config referencing 'WorkspaceId'), and create a corresponding migration that adds the index on the Projects table (and a down migration to drop it) so existing databases get the new index.src/Models/ProjectNotificationSubscription.ts (1)
58-58: ⚡ Quick winValidate
Eventsat the model boundary.
DataTypes.JSONaccepts any shape; add a validator to enforce non-empty array + enum membership so malformed events don’t get persisted.Proposed diff
- Events: { type: DataTypes.JSON, allowNull: false, field: 'Events' }, + Events: { + type: DataTypes.JSON, + allowNull: false, + field: 'Events', + validate: { + isValidEvents(value: unknown) { + if (!Array.isArray(value) || value.length === 0) { + throw new Error('Events must be a non-empty array'); + } + const allowed = new Set(Object.values(ENotificationEvent)); + if (!value.every((event) => allowed.has(event as ENotificationEvent))) { + throw new Error('Events contains unsupported notification event(s)'); + } + }, + }, + },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Models/ProjectNotificationSubscription.ts` at line 58, The Events attribute on the ProjectNotificationSubscription model currently uses DataTypes.JSON without validation; add a validator on the Events field (in ProjectNotificationSubscription) that ensures the value is an array, not empty, and that every item is one of the allowed event names (use the project's canonical event enum/constant or inline allowed list), throwing a validation error if any check fails so malformed event payloads cannot be persisted.src/Routes/EnvironmentVariableRoutes.ts (1)
27-27: ⚡ Quick winRename
InitializeRoutesto camelCase.Use
initializeRoutesto match the TypeScript naming rule for function names.As per coding guidelines
**/*.{ts,tsx}: Use camelCase for function names and variable names.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Routes/EnvironmentVariableRoutes.ts` at line 27, Rename the method InitializeRoutes to camelCase initializeRoutes in the EnvironmentVariableRoutes class: update the method declaration name from InitializeRoutes to initializeRoutes, update every internal call site or override that references InitializeRoutes (e.g., any constructors or lifecycle setup code that invokes this method), and adjust any tests or exports that reference the old symbol so all references use initializeRoutes consistently; ensure TypeScript compiles and no stray references to InitializeRoutes remain.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 03964a42-b3bd-4717-badf-27721ad136cb
⛔ Files ignored due to path filters (3)
bun.lockis excluded by!**/*.lockdocs/screenshots/projects.jpgis excluded by!**/*.jpgpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (135)
.env.example.gitignoreCLAUDE.mdREADME.md__tests__/helpers/.gitkeep__tests__/helpers/factories.ts__tests__/helpers/redis.ts__tests__/helpers/setupTestDb.ts__tests__/helpers/testApp.ts__tests__/helpers/token.ts__tests__/integration/.gitkeep__tests__/integration/Auth.test.ts__tests__/integration/Deployments.test.ts__tests__/integration/EnvVars.test.ts__tests__/integration/Notifications.test.ts__tests__/integration/Projects.test.ts__tests__/integration/QueueAdmin.test.ts__tests__/integration/Rollback.test.ts__tests__/integration/SocketLogStream.test.ts__tests__/integration/Users.test.ts__tests__/unit/.gitkeep__tests__/unit/Config/.gitkeep__tests__/unit/Models/.gitkeep__tests__/unit/Services/.gitkeep__tests__/unit/Services/AuditLogService.test.ts__tests__/unit/Services/AutoRecovery.test.ts__tests__/unit/Services/NotificationService.test.ts__tests__/unit/Services/Notifications/.gitkeep__tests__/unit/Services/Notifications/DiscordDispatcher.test.ts__tests__/unit/Services/Notifications/EmailDispatcher.test.ts__tests__/unit/Services/Notifications/SlackDispatcher.test.ts__tests__/unit/Services/QueueService.test.ts__tests__/unit/Types/.gitkeep__tests__/unit/Types/WorkspaceIconsParity.test.ts__tests__/unit/Utils/.gitkeep__tests__/unit/Utils/EncryptionHelper.test.ts__tests__/unit/Utils/LogFormatter.test.ts__tests__/unit/Utils/PasswordHelper.test.ts__tests__/unit/Utils/SshKeyGenerator.test.tsdocs/CHANGELOG.mddocs/ROADMAP.mddocs/migration-v2-to-v3.mddocs/release-engineering.mddocs/test-coverage-status.mddocs/v3.0-staging-verification.mddocs/versions/v3.0-foundation.mddocs/versions/v3.1-remote-targets.mddocs/versions/v3.2-governance.mddocs/versions/v3.3-strategies.mddocs/versions/v4.0-enterprise.mddocs/versions/v4.1-containers.mddocs/versions/v4.2-ai-ops.mddocs/versions/v5.0-cloud-native.mdjest.config.jspackage.jsonpublic/assets/chart-vendor-BXLXlBHU.jspublic/assets/index-CDAGGr3f.jspublic/assets/index-CTUrqtMF.jspublic/assets/mui-vendor-B8dvy3cB.jspublic/assets/mui-vendor-Bx2cJiJa.jspublic/assets/react-query-vendor-CLcLhmDs.jspublic/assets/stylis-plugin-rtl-vendor-CnNPRywY.jspublic/index.htmlsrc/App.tssrc/Config/AppConfig.tssrc/Config/RedisConfig.tssrc/Controllers/DeploymentController.tssrc/Controllers/EnvironmentVariableController.tssrc/Controllers/NotificationChannelController.tssrc/Controllers/NotificationProviderController.tssrc/Controllers/ProjectNotificationSubscriptionController.tssrc/Controllers/ProjectTemplateController.tssrc/Controllers/UsersController.tssrc/Controllers/WorkspaceController.tssrc/Database/DatabaseInitializer.tssrc/Database/MigrationRunner.tssrc/Middleware/CsrfMiddleware.tssrc/Middleware/QueueReadyMiddleware.tssrc/Middleware/SecurityMiddleware.tssrc/Migrations/009_create_environment_variables.tssrc/Migrations/012_add_queue_job_id_to_deployments.tssrc/Migrations/013_create_notification_providers.tssrc/Migrations/016_create_workspaces.tssrc/Migrations/017_create_project_templates.tssrc/Migrations/018_create_notification_channels.tssrc/Migrations/019_create_project_notification_subscriptions.tssrc/Migrations/020_drop_user_notification_columns.tssrc/Migrations/999_migrate_pending_deployments.tssrc/Models/Deployment.tssrc/Models/EnvironmentVariable.tssrc/Models/NotificationChannel.tssrc/Models/NotificationProvider.tssrc/Models/Project.tssrc/Models/ProjectNotificationSubscription.tssrc/Models/ProjectTemplate.tssrc/Models/UserSettings.tssrc/Models/Workspace.tssrc/Models/index.tssrc/Routes/DeploymentRoutes.tssrc/Routes/EnvironmentVariableRoutes.tssrc/Routes/NotificationChannelRoutes.tssrc/Routes/NotificationProviderRoutes.tssrc/Routes/ProjectNotificationSubscriptionRoutes.tssrc/Routes/ProjectTemplateRoutes.tssrc/Routes/UsersRoutes.tssrc/Routes/WebhookRoutes.tssrc/Routes/WorkspaceRoutes.tssrc/Routes/index.tssrc/Server.tssrc/Services/DeploymentService.tssrc/Services/EnvironmentVariableService.tssrc/Services/NotificationChannelService.tssrc/Services/NotificationProviderService.tssrc/Services/NotificationService.tssrc/Services/Notifications/DiscordDispatcher.tssrc/Services/Notifications/EmailDispatcher.tssrc/Services/Notifications/INotificationDispatcher.tssrc/Services/Notifications/SlackDispatcher.tssrc/Services/PipelineService.tssrc/Services/ProjectNotificationSubscriptionService.tssrc/Services/ProjectService.tssrc/Services/ProjectTemplateService.tssrc/Services/QueueAdminService.tssrc/Services/QueueService.tssrc/Services/RollbackService.tssrc/Services/SocketService.tssrc/Services/UserSettingsService.tssrc/Services/WorkspaceService.tssrc/Types/ICommon.tssrc/Types/IDatabase.tssrc/Types/IWorkspaceIcons.tssrc/Utils/LogFormatter.tssrc/Utils/ResponseHelper.tssrc/scripts/migrate.tstsconfig.test.json
💤 Files with no reviewable changes (1)
- src/Controllers/UsersController.ts
| this.Router.get( | ||
| '/:id/log/download', | ||
| this.AuthMiddleware.Authenticate, | ||
| this.RateLimiter.ApiLimiter, | ||
| this.DeploymentController.DownloadDeploymentLog | ||
| ); |
There was a problem hiding this comment.
Add deployment access control to log download route.
Line 86 currently authenticates but does not enforce per-deployment authorization, unlike the logs route. This can leak deployment logs across projects.
Suggested fix
this.Router.get(
'/:id/log/download',
this.AuthMiddleware.Authenticate,
+ this.DeploymentAccessMiddleware.CheckDeploymentAccess,
this.RateLimiter.ApiLimiter,
this.DeploymentController.DownloadDeploymentLog
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| this.Router.get( | |
| '/:id/log/download', | |
| this.AuthMiddleware.Authenticate, | |
| this.RateLimiter.ApiLimiter, | |
| this.DeploymentController.DownloadDeploymentLog | |
| ); | |
| this.Router.get( | |
| '/:id/log/download', | |
| this.AuthMiddleware.Authenticate, | |
| this.DeploymentAccessMiddleware.CheckDeploymentAccess, | |
| this.RateLimiter.ApiLimiter, | |
| this.DeploymentController.DownloadDeploymentLog | |
| ); |
🧰 Tools
🪛 GitHub Check: CodeQL
[failure] 86-86: Missing rate limiting
This route handler performs authorization, but is not rate-limited.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Routes/DeploymentRoutes.ts` around lines 84 - 89, The download log route
currently only authenticates but does not perform per-deployment authorization,
so update the route registration that uses Router.get('/:id/log/download',
AuthMiddleware.Authenticate, RateLimiter.ApiLimiter,
DeploymentController.DownloadDeploymentLog) to also include the same
per-deployment authorization middleware used by the other logs route (the
middleware that checks project/deployment membership) before calling
DeploymentController.DownloadDeploymentLog; locate and reuse the existing
authorization middleware symbol from the logs route (e.g.,
DeploymentMiddleware.AuthorizeDeployment or
ProjectMiddleware.EnsureDeploymentAccess) so the download endpoint enforces the
same access control as the other log endpoint.
| await this.RunGit( | ||
| `git clone --bare ${project.RepoUrl} "${cachePath}"`, | ||
| cacheParent, | ||
| sshKeyContext, | ||
| 300000 // 5 min | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Git shell command construction in DeploymentService =="
rg -n -C2 --type=ts 'execAsync\(|ExecuteGitCommandWithKey\(' src/Services/DeploymentService.ts src/Utils/SshKeyManager.ts
echo
echo "== RepoUrl / Branch sources and validation paths =="
rg -n -C3 --type=ts '\bRepoUrl\b|\bBranch\b' src/Controllers src/Routes src/Services src/ModelsRepository: FutureSolutionDev/Deploy-Center-Server
Length of output: 41530
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find execAsync definition/implementation =="
rg -n --type=ts "(\bexecAsync\b\s*=|\bfunction\s+execAsync\b|export\s+(async\s+)?function\s+execAsync\b|export\s+\{\s*execAsync\s*\}|from\s+['\"][^'\"]+['\"])" src
echo
echo "== Find where DeploymentService imports execAsync (to identify source module) =="
rg -n --type=ts "import\s+.*execAsync|require\(.*execAsync" src/Services/DeploymentService.ts src/Utils/SshKeyManager.ts src
echo
echo "== Inspect DeploymentService RunGit and the clone/checkout call sites around lines ~215-220 and other referenced ranges =="
sed -n '180,260p' src/Services/DeploymentService.ts
sed -n '1120,1260p' src/Services/DeploymentService.ts
sed -n '1320,1380p' src/Services/DeploymentService.ts
echo
echo "== Inspect SshKeyManager ExecuteGitCommandWithKey around the execAsync call =="
sed -n '220,340p' src/Utils/SshKeyManager.ts
echo
echo "== Check Project model validation for RepoUrl/Branch (if present) =="
rg -n --type=ts "isValidGitUrl|RepoUrl|Branch" src/Models/ src/Models
fd -t f "Project.*" src/Models -a 2>/dev/null || true
ls -la src/Models || true
for f in $(fd -t f -e ts "Project" src/Models || true); do echo "--- $f"; sed -n '1,220p' "$f"; doneRepository: FutureSolutionDev/Deploy-Center-Server
Length of output: 47500
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== DeploymentService RunGit + lines ~200-240 =="
sed -n '180,260p' src/Services/DeploymentService.ts
echo
echo "== Clone/checkout command construction around ~1160-1260 (covers earlier mention 1191-1197) =="
sed -n '1160,1280p' src/Services/DeploymentService.ts
echo
echo "== Any other git commands using project.RepoUrl / deployment.Branch (broader context) =="
rg -n --type=ts 'git (clone|checkout|ls-remote|rev-parse)' src/Services/DeploymentService.ts
echo
echo "== ExecAsync source: DeploymentService execAsync definition =="
sed -n '90,140p' src/Services/DeploymentService.ts
echo
echo "== SshKeyManager ExecuteGitCommandWithKey implementation around execAsync call =="
sed -n '230,330p' src/Utils/SshKeyManager.ts
echo
echo "== Project model validation for RepoUrl/Branch (if any) =="
sed -n '1,240p' src/Models/Project.ts
rg -n --type=ts 'RepoUrl|Branch|isValidGitUrl|NormalizeGitUrl|Joi|zod|validate' src/Models/Project.ts src/Services/WebhookService.ts src/Services/ProjectService.ts src/Controllers/ProjectController.tsRepository: FutureSolutionDev/Deploy-Center-Server
Length of output: 22897
Avoid shell-interpolating RepoUrl and Branch into git command strings in DeploymentService.
DeploymentService (and SshKeyManager) executes these template-string commands via execAsync = promisify(exec) (child_process.exec), which runs through a shell—so untrusted values containing shell metacharacters can escape and execute arbitrary input.
EnsureBareCache:git clone --bare ${project.RepoUrl} "${cachePath}"(src/Services/DeploymentService.ts ~215-220) interpolatesproject.RepoUrlwithout quoting/escaping.PrepareRepository:git clone --branch ${deployment.Branch} ... ${project.RepoUrl} .(also applies to ~1191-1197) interpolatesdeployment.Branchandproject.RepoUrlwithout quoting/escaping.Projectmodel only validatesRepoUrlwith permissive regexes (.+) and there’s noBranchvalidation, so this isn’t sufficient to make interpolation safe.
Replace with argv-based execution (spawn/execFile) and pass git args as an array, or enforce strict allowlists for RepoUrl/Branch before use.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Services/DeploymentService.ts` around lines 215 - 220, The git commands
in EnsureBareCache and PrepareRepository (invoked via RunGit) interpolate
untrusted project.RepoUrl and deployment.Branch into shell strings, enabling
shell injection; replace these exec-based template strings with argv-based
execution (child_process.spawn or execFile) by changing RunGit to accept a
command array (e.g., ['clone','--bare', project.RepoUrl, cachePath]) and invoke
git with those args, and update calls in EnsureBareCache and PrepareRepository
to pass args instead of interpolated strings; alternatively, if you must keep
exec, add strict validation/allowlists for project.RepoUrl and deployment.Branch
in the Project model and SshKeyManager before interpolating, but prefer the
spawn/execFile approach for RunGit to eliminate shell interpretation.
Multi-agent local review of PR #80 (the v3.0.0 GA candidate) surfaced 5 CRITICAL and ~13 HIGH issues. This commit fixes all of them plus the selected MEDIUMs that affect correctness or observability. CRITICAL ======== - MigrationRunner.ts:16 — slot 007 incorrectly imported migration 008's body (duplicate import). The real Deployments.{ErrorMessage,CommitMessage} TEXT → LONGTEXT widening never ran on any install. Delete the broken "007 ... copy.ts" file, drop slot 007 from MigrationRunner (slot 008 covers the same idempotent DDL), add migration 021 that does the real Deployments widening. - DeploymentAccessMiddleware — Manager role was missing from BOTH CheckDeploymentAccess and CheckDeploymentModifyAccess. Managers fell through to "Insufficient permissions", silently locking them out of rollback / retry / cancel — breaking the F-007 contract. - Routes/index.ts — PATCH /api/projects/:projectId/workspace had only Authenticate; any logged-in user (including Viewer or a Developer who is NOT a project member) could move any project into/out of any workspace. Add ProjectAccessMiddleware.CheckProjectModifyAccess. - package.json — version was still "2.1.2" while CHANGELOG / CLAUDE.md / tag say 3.0.0. Bumped. HIGH ==== - CancelDeployment never removed the BullMQ job — "cancelled" deployments still executed because the worker picked up the queued job and ran it. Two-step fix: queue.getJob(jobId).remove() + a cancel-aware guard at the top of ExecuteDeployment that bails if Status === Cancelled. - RollbackService non-atomic: create row → enqueue (Redis side-effect) → audit. If audit threw, an orphan Queued deployment + enqueued job were left in a partial state. Wrap row+audit in a SQL transaction; on commit failure, compensate by removing the BullMQ job we already added. - EnvironmentVariableController had no project-exists guard — Admin could blindly hit /api/projects/999999/env-vars and get [] back instead of 404; Create silently linked rows to a non-existent project. Add requireProjectId() + use Conflict (409) on duplicate-key. - CI workflow only ran on `branches: [main]` — the master / version branches never triggered CI. Expand to [main, master, "version/**"] and add MariaDB + Redis services so integration tests actually run instead of silently auto-skipping on `if (!dbUp) return`. - ESLint v9 + legacy `.eslintrc.json` meant `npm run lint` silently no-op'd (exit 0 always). Pinned eslint to ^8.57.1 + @typescript-eslint/* to ^7.18 + relaxed the most-violated rules to "warn" so the CI gate is enforced without blocking on 800+ lines of pre-existing tech debt. - ts-jest tsconfig was pointing at the strict app tsconfig.json instead of the new tsconfig.test.json — dead config file. Wired it correctly. - QueueAdmin.test.ts used the shared getTestRedis() probe (lazyConnect: false) which enters ioredis's reconnect-forever loop when Redis is down, holding jest open for the full 20s timeout. Replaced with the throwaway-probe pattern from QueueService.test.ts (retryStrategy: () => null + connectTimeout: 2000). - Notifications.test.ts mutated NotificationService.Dispatchers singleton without restoring it in afterAll — any later suite would silently hit the jest.fn from this file. Save+restore originals. - ProjectFormModal err-typing aligned with the rest of the codebase (in-narrow type check instead of `as any`). - Migration 999 — wrap audit-INSERTs in try/catch (logged but non-fatal) so audit failures never fail the migration. SQL transaction would have CAUSED the duplicate-enqueue race, not prevented it (Redis side-effect is not rollback-able from SQL); documented this in the file header. - migration-v2-to-v3.md + CHANGELOG.md — synced the migration order + table to match the actual MigrationRunner array; added rows for 020 + 021. - .env.example — added DB_FORCE_SYNC_ALTER + RUN_LONG_STREAM_TEST + RUN_SLOW_QUEUE_TEST documentation comments. MEDIUM ====== - NotificationService — `process.env.NODE_ENV ?? 'production'` fallback (4 sites) to avoid literal "undefined" appearing in payloads. - QueueService — extracted QUEUE_PRIORITY constants (Rollback=1, Manual=10, Webhook=0) and wired RollbackService + DeploymentService to use them. BullMQ semantics: lower number = higher priority. - ResponseHelper.Conflict (409) used for duplicate-key in EnvironmentVariableController (was 400). - Removed stray console.log() in DeploymentService.CreateDeployment + DeploymentController.CreateManualDeployment. - SecurityMiddleware excludedFields — de-duplicated 'Config' (was listed twice); replaced too-broad lowercase 'channel' with PascalCase 'Channel' to avoid shadowing future fields. - SocketService.EmitRollbackQueued no longer double-broadcasts (was emitting globally AND to the same room). - Migration 017 — explicit named unique constraint matching the model's `uniq_project_templates_name`, instead of inline `unique: true` (which would conflict if sync({ alter: true }) ever ran). - Migration 016 down() — simplified to drop column (which cascades FK) instead of trying to removeConstraint by an unknown auto-name. All three tsc invocations remain clean (server, server tests, client). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build-test.yml + lint.yml only triggered on [main, develop] previously, so the v3.0 PR against master and all version/v3.x branches never ran CI. Expand to [main, master, develop, "version/**"]. build-test.yml also gains a Redis service container so the v3.0 F-001 integration suites (QueueAdmin, Rollback, Deployments full flow) can exercise the BullMQ path instead of silently auto-skipping on `if (!redisUp) return`. MariaDB image bumped 10.6 → 11 to match the production target documented in docker-compose.yml. REDIS_HOST/PORT/DB env vars wired through to the test step; REDIS_DB=1 keeps test data off DB 0 per the helpers/redis.ts convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Deploy-Center-Server/src/Controllers/AuthController.ts
Lines 90 to 95 in cdbd305
The public registration endpoint forwards the request body Role directly into user creation, allowing an unauthenticated caller to self-assign privileged roles (for example admin or manager) at account creation. This is a direct privilege-escalation path because /api/auth/register has no authentication or admin-only guard.
Deploy-Center-Server/src/Services/SocketService.ts
Lines 62 to 64 in cdbd305
Socket connections are accepted and allowed to join arbitrary deployment:{id} rooms with no authentication or authorization check, which lets an unauthenticated client subscribe to real-time deployment log events by emitting join:deployment with guessed IDs. Because deployment logs are emitted to these rooms, this exposes cross-tenant deployment activity and log data.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| '/:id/log/download', | ||
| this.AuthMiddleware.Authenticate, | ||
| this.RateLimiter.ApiLimiter, | ||
| this.DeploymentController.DownloadDeploymentLog | ||
| ); |
There was a problem hiding this comment.
Enforce deployment access on log download route
This route is missing CheckDeploymentAccess, so any authenticated user can request /api/deployments/:id/log/download for deployments they do not own and retrieve raw deployment logs (which may include sensitive operational details). The neighboring /:id/logs endpoint correctly applies deployment-level authorization, so this path currently creates a privilege bypass for log exfiltration.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/Middleware/SecurityMiddleware.ts (1)
349-365:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDrop global (
g/gi) flags from the SQL detector regexes (or resetlastIndexbefore eachtest)
PreventSQLInjectionreuses the samesqlPatternsregex instances across multiplecheckSQL()calls for different strings. With/g//gi,RegExp.prototype.test()is stateful vialastIndex, which can cause later matches to be skipped and produce false negatives. Removeg/gifrom these detector regexes (or setpattern.lastIndex = 0before everypattern.test(value)).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Middleware/SecurityMiddleware.ts` around lines 349 - 365, The sql detector regexes in sqlPatterns (used by PreventSQLInjection and its checkSQL calls) include global flags (/g or /gi) which make RegExp.test stateful and can skip matches; remove the g/gi flags from those regex literals (or, alternatively, ensure each pattern's lastIndex is reset to 0 before testing by setting pattern.lastIndex = 0 inside checkSQL) so pattern.test(value) behaves deterministically across repeated calls.src/Migrations/017_create_project_templates.ts (1)
205-218:⚠️ Potential issue | 🟠 Major | ⚡ Quick winInsert
DefaultConfigas JSON, not a pre-serialized string.
DefaultConfigis declared asDataTypes.JSON, but this seed turns each object into a string before insert. On MariaDB/MySQL that stores a JSON string value instead of the expected object, so template consumers will read back the wrong shape.Suggested fix
- DefaultConfig: JSON.stringify(t.DefaultConfig), + DefaultConfig: t.DefaultConfig,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Migrations/017_create_project_templates.ts` around lines 205 - 218, The seed is serializing DefaultConfig before insert which stores it as a string; update the toInsert mapping (the variable toInsert created from SEED_TEMPLATES) so DefaultConfig is assigned the original object (DefaultConfig: t.DefaultConfig) instead of JSON.stringify(t.DefaultConfig), then call queryInterface.bulkInsert(TABLE, toInsert, { transaction }) as before; this preserves the JSON type expected by the DataTypes.JSON column.src/Services/DeploymentService.ts (1)
444-452:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftMake “queued” mean “job exists” in
CreateDeployment().This creates the row with
Status=Queuedbefore the BullMQ add succeeds, and it persistsQueueJobIdonly afterward. IfEnqueue()throws, the DB is left with a queued deployment that will never run; if enqueue succeeds and the follow-upsave()fails, the worker can still run the job but later cancellation/re-enqueue logic loses the job correlation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Services/DeploymentService.ts` around lines 444 - 452, The deployment row is being persisted as Status=Queued before the BullMQ job is created, causing orphaned queued rows if Enqueue() fails or save() fails after enqueue; change CreateDeployment() to call QueueService.Enqueue(...) first, capture the returned jobId, then set deployment.QueueJobId and deployment.Status = 'Queued' and persist the row (deployment.save()) — alternatively wrap the DB create/update and enqueue in an atomic flow: create the deployment in a non-queued state (or in a transaction), call QueueService.Enqueue(deployment.Id, JsonProject.Id, ...), then update deployment.QueueJobId and Status and save; ensure usage sites are the CreateDeployment function and symbols QueueService.Enqueue, deployment.QueueJobId, deployment.Status and deployment.save().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@package.json`:
- Line 3: The deployment metadata writer is still hardcoded to
deployCenterVersion: '2.1.1' in DeploymentService.WriteDeploymentMetadata(),
causing .deploy-center files to advertise the wrong server version after
package.json was bumped to 3.0.0; update WriteDeploymentMetadata() to emit the
correct version (either by replacing the hardcoded '2.1.1' with '3.0.0' or,
better, read the version from package.json at build/runtime and use that value
for deployCenterVersion) so the produced metadata matches package.json.
In `@src/Database/MigrationRunner.ts`:
- Around line 16-21: The migration registry must retain a no-op placeholder for
slot "007" so rollback and status logic can find an executed migration; update
the Migrations registry (where migration slots are defined/registered in
MigrationRunner) to re-add a placeholder entry for "007" that performs no schema
changes, ensuring RollbackLastMigration() and GetStatus() can locate the
migration row and that the placeholder is idempotent—implement the entry as a
no-op migration (empty up/down or a marker) and include a clear comment
referencing the duplicate-import bug and that real DDL lives in migration "021".
In `@src/Middleware/DeploymentAccessMiddleware.ts`:
- Around line 153-158: The middleware grants EUserRole.Manager modify access but
DeploymentController.CancelDeployment and RetryDeployment still block
non-Admin/Developer roles; update those controller methods (CancelDeployment and
RetryDeployment) to include EUserRole.Manager in their role-allowlist or,
better, centralize authorization by calling the same check used in
DeploymentAccessMiddleware (the user.Role/EUserRole comparison) so Managers
accepted by the middleware aren't later rejected—ensure all role checks
reference the same logic (including EUserRole.Manager) to avoid the late 403.
In `@src/Migrations/021_widen_deployments_text_columns.ts`:
- Around line 76-97: In the down() of migration
021_widen_deployments_text_columns, add a row-level guard before calling
queryInterface.changeColumn to narrow LONGTEXT → TEXT: after obtaining desc via
describeTable and before changeColumn for ErrorMessage/CommitMessage, run a raw
query that counts rows where LENGTH(column) > 65535 (e.g.,
SUM(LENGTH(ErrorMessage) > 65535) and SUM(LENGTH(CommitMessage) > 65535)); if
any count > 0, skip/abort the narrowing and log a clear message instead of
executing changeColumn; keep the existing isAlreadyLongtext checks and only
perform the LENGTH check for columns that passed that metadata check so
changeColumn is only called when safe.
In `@src/Routes/DeploymentRoutes.ts`:
- Around line 157-163: The route allows any authenticated user with a global
developer role to start a deployment for any :projectId because there is no
project-scoped permission check before CreateManualDeployment; add a middleware
that enforces project-level membership/modify rights (e.g.,
ProjectMiddleware.RequireProjectMemberOrManager or
ProjectMiddleware.RequireProjectModifyAccess) into the chain before
this.DeploymentController.CreateManualDeployment and ensure that middleware
validates the :projectId param and rejects requests when the caller is not a
member or lacks modify/deploy permission.
In `@src/Services/DeploymentService.ts`:
- Around line 2166-2195: Switch the order so the deployment row is marked
Cancelled and persisted before attempting to remove the BullMQ job: set
deployment.Status = EDeploymentStatus.Cancelled, set deployment.CompletedAt =
new Date(), await deployment.save() first (this ensures ExecuteDeployment will
short-circuit any worker), then call this.QueueService.GetBullMqQueue(), fetch
job via queue.getJob(deployment.QueueJobId) and attempt job.remove() as
best-effort; keep the existing try/catch around the job removal and log failures
with Logger.Warn but do not attempt to roll back the DB update if job removal
fails (referencing DeploymentService, deployment.QueueJobId,
QueueService.GetBullMqQueue, ExecuteDeployment, and
EDeploymentStatus.Cancelled).
In `@src/Services/NotificationService.ts`:
- Line 212: SendSlackNotification in NotificationService currently uses raw
process.env.NODE_ENV which can be undefined; update SendSlackNotification to use
the same fallback as the other renderers (process.env.NODE_ENV ?? 'production')
when building the Slack message/payload. Locate the SendSlackNotification method
and replace direct reads of process.env.NODE_ENV with the fallback expression,
and apply the same change to the other Slack-sending helper calls referenced in
the file (the occurrences noted around the other Slack send points) so Slack
messages match the other channels.
In `@src/Services/RollbackService.ts`:
- Around line 111-165: The code currently enqueues the rollback job
(QueueService.GetInstance().Enqueue) while rollbackDeployment is still inside an
open SQL transaction, which can let a worker race the commit; instead, create
the rollback Deployment row and AuditLog inside the transaction (using
Deployment.create and AuditLog.create), commit the transaction (tx.commit())
first, then call QueueService.GetInstance().Enqueue(rollbackDeployment.Id, ...)
outside the transaction, set rollbackDeployment.QueueJobId = enqueuedJobId and
persist via rollbackDeployment.save() (no transaction) and handle enqueue
failures by updating deployment status/logging (or deleting the enqueued job if
necessary) to avoid exposing uncommitted state to workers.
---
Outside diff comments:
In `@src/Middleware/SecurityMiddleware.ts`:
- Around line 349-365: The sql detector regexes in sqlPatterns (used by
PreventSQLInjection and its checkSQL calls) include global flags (/g or /gi)
which make RegExp.test stateful and can skip matches; remove the g/gi flags from
those regex literals (or, alternatively, ensure each pattern's lastIndex is
reset to 0 before testing by setting pattern.lastIndex = 0 inside checkSQL) so
pattern.test(value) behaves deterministically across repeated calls.
In `@src/Migrations/017_create_project_templates.ts`:
- Around line 205-218: The seed is serializing DefaultConfig before insert which
stores it as a string; update the toInsert mapping (the variable toInsert
created from SEED_TEMPLATES) so DefaultConfig is assigned the original object
(DefaultConfig: t.DefaultConfig) instead of JSON.stringify(t.DefaultConfig),
then call queryInterface.bulkInsert(TABLE, toInsert, { transaction }) as before;
this preserves the JSON type expected by the DataTypes.JSON column.
In `@src/Services/DeploymentService.ts`:
- Around line 444-452: The deployment row is being persisted as Status=Queued
before the BullMQ job is created, causing orphaned queued rows if Enqueue()
fails or save() fails after enqueue; change CreateDeployment() to call
QueueService.Enqueue(...) first, capture the returned jobId, then set
deployment.QueueJobId and deployment.Status = 'Queued' and persist the row
(deployment.save()) — alternatively wrap the DB create/update and enqueue in an
atomic flow: create the deployment in a non-queued state (or in a transaction),
call QueueService.Enqueue(deployment.Id, JsonProject.Id, ...), then update
deployment.QueueJobId and Status and save; ensure usage sites are the
CreateDeployment function and symbols QueueService.Enqueue,
deployment.QueueJobId, deployment.Status and deployment.save().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fa3c3bb5-1296-47c4-b954-42738b23ec75
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (27)
.env.example.eslintrc.json.github/workflows/build-test.yml.github/workflows/lint.yml__tests__/integration/Notifications.test.ts__tests__/integration/QueueAdmin.test.tsdocs/CHANGELOG.mddocs/migration-v2-to-v3.mdjest.config.jspackage.jsonsrc/Controllers/DeploymentController.tssrc/Controllers/EnvironmentVariableController.tssrc/Database/MigrationRunner.tssrc/Middleware/DeploymentAccessMiddleware.tssrc/Middleware/SecurityMiddleware.tssrc/Migrations/007_increase_deployment_steps_errormessage_and_commitmessage_size copy.tssrc/Migrations/016_create_workspaces.tssrc/Migrations/017_create_project_templates.tssrc/Migrations/021_widen_deployments_text_columns.tssrc/Migrations/999_migrate_pending_deployments.tssrc/Routes/DeploymentRoutes.tssrc/Routes/index.tssrc/Services/DeploymentService.tssrc/Services/NotificationService.tssrc/Services/QueueService.tssrc/Services/RollbackService.tssrc/Services/SocketService.ts
💤 Files with no reviewable changes (1)
- src/Migrations/007_increase_deployment_steps_errormessage_and_commitmessage_size copy.ts
✅ Files skipped from review due to trivial changes (2)
- docs/CHANGELOG.md
- docs/migration-v2-to-v3.md
| { | ||
| "name": "deploy-center-server", | ||
| "version": "2.1.2", | ||
| "version": "3.0.0", |
There was a problem hiding this comment.
Update the emitted deployment metadata version alongside this release bump.
package.json is now 3.0.0, but DeploymentService.WriteDeploymentMetadata() still writes deployCenterVersion: '2.1.1'. Every .deploy-center file produced by this release will therefore advertise the wrong server version.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@package.json` at line 3, The deployment metadata writer is still hardcoded to
deployCenterVersion: '2.1.1' in DeploymentService.WriteDeploymentMetadata(),
causing .deploy-center files to advertise the wrong server version after
package.json was bumped to 3.0.0; update WriteDeploymentMetadata() to emit the
correct version (either by replacing the hardcoded '2.1.1' with '3.0.0' or,
better, read the version from package.json at build/runtime and use that value
for deployCenterVersion) so the produced metadata matches package.json.
| // v3.0 fix: the old slot 007 incorrectly imported migration 008's body (duplicate | ||
| // import bug); the real `Deployments.{ErrorMessage,CommitMessage}` LONGTEXT | ||
| // widening lives in migration 021 below. Slot 007 is intentionally absent — | ||
| // removing it leaves only an orphan `007_...` row in SequelizeMeta on installs | ||
| // that already executed the broken slot. Harmless: not re-running an idempotent | ||
| // migration on a fresh install is fine because slot 008 covers the same DDL. |
There was a problem hiding this comment.
Keep a placeholder for migration 007 in the registry.
Removing slot 007 from Migrations makes upgrade states that already recorded it impossible to unwind cleanly: once rollback reaches that row, RollbackLastMigration() throws Migration ... not found, and GetStatus() silently hides the executed migration. Keep 007 registered as a no-op placeholder instead of deleting the slot. Based on learnings: "Never modify migration files after they have been run in the database".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Database/MigrationRunner.ts` around lines 16 - 21, The migration registry
must retain a no-op placeholder for slot "007" so rollback and status logic can
find an executed migration; update the Migrations registry (where migration
slots are defined/registered in MigrationRunner) to re-add a placeholder entry
for "007" that performs no schema changes, ensuring RollbackLastMigration() and
GetStatus() can locate the migration row and that the placeholder is
idempotent—implement the entry as a no-op migration (empty up/down or a marker)
and include a clear comment referencing the duplicate-import bug and that real
DDL lives in migration "021".
| // Admin + Manager have full modify access (Manager is the | ||
| // project-management role per the v3.0 RBAC matrix; without this | ||
| // branch, Manager would fall through to "Insufficient permissions", | ||
| // breaking rollback/retry/cancel for Manager-role users — explicitly | ||
| // promised in the F-007 contract). | ||
| if (user.Role === EUserRole.Admin || user.Role === EUserRole.Manager) { |
There was a problem hiding this comment.
Manager modify access is still inconsistent across the stack.
This middleware now lets Managers through for modify actions, but DeploymentController.CancelDeployment and RetryDeployment still reject any role except Admin/Developer. Managers will pass authorization here and then get a late 403 in the controller.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Middleware/DeploymentAccessMiddleware.ts` around lines 153 - 158, The
middleware grants EUserRole.Manager modify access but
DeploymentController.CancelDeployment and RetryDeployment still block
non-Admin/Developer roles; update those controller methods (CancelDeployment and
RetryDeployment) to include EUserRole.Manager in their role-allowlist or,
better, centralize authorization by calling the same check used in
DeploymentAccessMiddleware (the user.Role/EUserRole comparison) so Managers
accepted by the middleware aren't later rejected—ensure all role checks
reference the same logic (including EUserRole.Manager) to avoid the late 403.
| // Down narrows back to TEXT (64KB) — UNSAFE for rows whose value already | ||
| // exceeds 64KB (MySQL will truncate silently). Document this explicitly. | ||
| const desc = (await queryInterface.describeTable(TABLE)) as Record<string, IColumnDescription>; | ||
|
|
||
| if (desc.ErrorMessage && isAlreadyLongtext(desc.ErrorMessage)) { | ||
| await queryInterface.changeColumn( | ||
| TABLE, | ||
| 'ErrorMessage', | ||
| { type: DataTypes.TEXT, allowNull: true }, | ||
| { transaction } | ||
| ); | ||
| console.log(`✅ Migration 021 down: ${TABLE}.ErrorMessage narrowed to TEXT (may truncate!)`); | ||
| } | ||
|
|
||
| if (desc.CommitMessage && isAlreadyLongtext(desc.CommitMessage)) { | ||
| await queryInterface.changeColumn( | ||
| TABLE, | ||
| 'CommitMessage', | ||
| { type: DataTypes.TEXT, allowNull: true }, | ||
| { transaction } | ||
| ); | ||
| console.log(`✅ Migration 021 down: ${TABLE}.CommitMessage narrowed to TEXT (may truncate!)`); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Run this SELECT against the target DB before executing migration 021 down.
# It is read-only and reports whether narrowing would truncate data.
mysql "$DATABASE_URL" -Nse "
SELECT
SUM(CHAR_LENGTH(ErrorMessage) > 65535) AS too_long_error_message,
SUM(CHAR_LENGTH(CommitMessage) > 65535) AS too_long_commit_message
FROM Deployments;
"Repository: FutureSolutionDev/Deploy-Center-Server
Length of output: 128
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Show the migration file around the referenced lines
FILE="src/Migrations/021_widen_deployments_text_columns.ts"
echo "=== File: $FILE (lines around 60-120) ==="
nl -ba "$FILE" | sed -n '60,130p'
# 2) Sanity: locate up/down blocks and the describeTable usage
echo
echo "=== Locate up/down and describeTable in $FILE ==="
rg -n "export const (up|down)|describeTable\\(|isAlreadyLongtext|changeColumn\\(" "$FILE"
# 3) Search other migrations for similar “down narrows LONGTEXT -> TEXT” patterns
echo
echo "=== Search other migrations for truncation/guard patterns ==="
rg -n "UNSAFE|truncate|LONGTEXT|changeColumn\\(|describeTable\\(|isAlreadyLongtext" src/Migrations || trueRepository: FutureSolutionDev/Deploy-Center-Server
Length of output: 232
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Migrations/021_widen_deployments_text_columns.ts"
echo "=== File: $FILE (lines 60-130) ==="
cat -n "$FILE" | sed -n '60,130p'
echo
echo "=== Locate up/down and describeTable/changeColumn in $FILE ==="
rg -n "export const (up|down)|describeTable\\(|isAlreadyLongtext\\(|changeColumn\\(" "$FILE"
echo
echo "=== Search other migrations for similar truncation/guard patterns ==="
rg -n "LONGTEXT|truncate|UNSAFE|isAlreadyLongtext|changeColumn\\(|describeTable\\(" src/Migrations || trueRepository: FutureSolutionDev/Deploy-Center-Server
Length of output: 12069
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Migrations/021_widen_deployments_text_columns.ts"
echo "=== File: $FILE (lines 1-90) ==="
cat -n "$FILE" | sed -n '1,90p'Repository: FutureSolutionDev/Deploy-Center-Server
Length of output: 4308
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for charset/collation usage (utf8mb4 etc.) ==="
rg -n "utf8mb4|CHARSET|collat|COLLATE|character set|characterSet" . || true
echo
echo "=== Search for LENGTH(.*)>65535 patterns or truncation guards in migrations ==="
rg -n "LENGTH\\(.*\\)>\\s*65535|CHAR_LENGTH\\(.*\\)>\\s*65535|may truncate|truncat" src/Migrations || true
echo
echo "=== Show down() remainder for migration 021 ==="
cat -n src/Migrations/021_widen_deployments_text_columns.ts | sed -n '90,130p'Repository: FutureSolutionDev/Deploy-Center-Server
Length of output: 2050
🏁 Script executed:
# Read-only check to run against the target DB before executing migration 021 down
mysql "$DATABASE_URL" -Nse "
SELECT
SUM(LENGTH(ErrorMessage) > 65535) AS too_long_error_message,
SUM(LENGTH(CommitMessage) > 65535) AS too_long_commit_message
FROM Deployments;
"Repository: FutureSolutionDev/Deploy-Center-Server
Length of output: 128
Add a row-level truncation guard in 021_widen_deployments_text_columns.down (LONGTEXT → TEXT).
down() only checks column metadata (describeTable + isAlreadyLongtext) and then always runs changeColumn(... DataTypes.TEXT ...) for LONGTEXT columns—so if any existing values are already >64KB bytes, MySQL can silently truncate them.
Guard by checking byte length before narrowing
SELECT
SUM(LENGTH(ErrorMessage) > 65535) AS tooLongErrorMessage,
SUM(LENGTH(CommitMessage) > 65535) AS tooLongCommitMessage
FROM Deployments;If either count is > 0, abort/skip the rollback instead of narrowing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Migrations/021_widen_deployments_text_columns.ts` around lines 76 - 97,
In the down() of migration 021_widen_deployments_text_columns, add a row-level
guard before calling queryInterface.changeColumn to narrow LONGTEXT → TEXT:
after obtaining desc via describeTable and before changeColumn for
ErrorMessage/CommitMessage, run a raw query that counts rows where
LENGTH(column) > 65535 (e.g., SUM(LENGTH(ErrorMessage) > 65535) and
SUM(LENGTH(CommitMessage) > 65535)); if any count > 0, skip/abort the narrowing
and log a clear message instead of executing changeColumn; keep the existing
isAlreadyLongtext checks and only perform the LENGTH check for columns that
passed that metadata check so changeColumn is only called when safe.
| this.Router.post( | ||
| '/projects/:projectId/deploy', | ||
| this.AuthMiddleware.Authenticate, | ||
| this.RoleMiddleware.RequireAdminOrDeveloper, | ||
| this.RoleMiddleware.RequireAdminManagerOrDeveloper, | ||
| this.RateLimiter.DeploymentLimiter, | ||
| RequireQueueReady, // F-001 FR-005b — 503 if Redis down | ||
| this.DeploymentController.CreateManualDeployment |
There was a problem hiding this comment.
Enforce project-scoped access before creating a manual deployment.
This chain only checks the caller's global role. Any authenticated developer can post to another project's :projectId and trigger a deployment because there is no project-level membership/modify-access middleware before CreateManualDeployment.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Routes/DeploymentRoutes.ts` around lines 157 - 163, The route allows any
authenticated user with a global developer role to start a deployment for any
:projectId because there is no project-scoped permission check before
CreateManualDeployment; add a middleware that enforces project-level
membership/modify rights (e.g., ProjectMiddleware.RequireProjectMemberOrManager
or ProjectMiddleware.RequireProjectModifyAccess) into the chain before
this.DeploymentController.CreateManualDeployment and ensure that middleware
validates the :projectId param and rejects requests when the caller is not a
member or lacks modify/deploy permission.
| // (a) Try to remove the BullMQ job. If Redis is down or the job has | ||
| // already moved to active, this fails — log and continue; the | ||
| // ExecuteDeployment guard will short-circuit a job that did slip | ||
| // through. We only remove jobs that have a known QueueJobId; for | ||
| // pre-v3.0 rows or rows mid-enqueue, QueueJobId is null. | ||
| if (deployment.QueueJobId) { | ||
| try { | ||
| const queue = this.QueueService.GetBullMqQueue(); | ||
| const job = await queue.getJob(deployment.QueueJobId); | ||
| if (job) { | ||
| await job.remove(); | ||
| Logger.Info('Removed BullMQ job for cancelled deployment', { | ||
| deploymentId, | ||
| jobId: deployment.QueueJobId, | ||
| }); | ||
| } | ||
| } catch (err) { | ||
| // Non-fatal — the row mark below + worker guard cover the case. | ||
| Logger.Warn('Failed to remove BullMQ job; relying on worker guard', { | ||
| deploymentId, | ||
| jobId: deployment.QueueJobId, | ||
| error: (err as Error).message, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // (b) Mark the row Cancelled. | ||
| deployment.Status = EDeploymentStatus.Cancelled; | ||
| deployment.CompletedAt = new Date(); | ||
| await deployment.save(); |
There was a problem hiding this comment.
Persist the cancellation before removing the BullMQ job.
The current order removes the external queue job first and only then saves Status=Cancelled. If that DB write or the audit path fails afterward, this row stays Queued even though no job exists anymore, so it becomes permanently unrunnable. Since ExecuteDeployment() now guards on Cancelled, the safer order is to commit the row cancellation first and treat job.remove() as best-effort after that.
Suggested direction
- if (deployment.QueueJobId) {
- try {
- const queue = this.QueueService.GetBullMqQueue();
- const job = await queue.getJob(deployment.QueueJobId);
- if (job) {
- await job.remove();
- Logger.Info('Removed BullMQ job for cancelled deployment', {
- deploymentId,
- jobId: deployment.QueueJobId,
- });
- }
- } catch (err) {
- Logger.Warn('Failed to remove BullMQ job; relying on worker guard', {
- deploymentId,
- jobId: deployment.QueueJobId,
- error: (err as Error).message,
- });
- }
- }
-
- deployment.Status = EDeploymentStatus.Cancelled;
- deployment.CompletedAt = new Date();
- await deployment.save();
+ deployment.Status = EDeploymentStatus.Cancelled;
+ deployment.CompletedAt = new Date();
+ await deployment.save();
+
+ if (deployment.QueueJobId) {
+ try {
+ const queue = this.QueueService.GetBullMqQueue();
+ const job = await queue.getJob(deployment.QueueJobId);
+ if (job) {
+ await job.remove();
+ Logger.Info('Removed BullMQ job for cancelled deployment', {
+ deploymentId,
+ jobId: deployment.QueueJobId,
+ });
+ }
+ } catch (err) {
+ Logger.Warn('Failed to remove BullMQ job; relying on worker guard', {
+ deploymentId,
+ jobId: deployment.QueueJobId,
+ error: (err as Error).message,
+ });
+ }
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Services/DeploymentService.ts` around lines 2166 - 2195, Switch the order
so the deployment row is marked Cancelled and persisted before attempting to
remove the BullMQ job: set deployment.Status = EDeploymentStatus.Cancelled, set
deployment.CompletedAt = new Date(), await deployment.save() first (this ensures
ExecuteDeployment will short-circuit any worker), then call
this.QueueService.GetBullMqQueue(), fetch job via
queue.getJob(deployment.QueueJobId) and attempt job.remove() as best-effort;
keep the existing try/catch around the job removal and log failures with
Logger.Warn but do not attempt to roll back the DB update if job removal fails
(referencing DeploymentService, deployment.QueueJobId,
QueueService.GetBullMqQueue, ExecuteDeployment, and
EDeploymentStatus.Cancelled).
| { | ||
| name: 'Deployment Environment', | ||
| value: process.env.NODE_ENV, | ||
| value: process.env.NODE_ENV ?? 'production', |
There was a problem hiding this comment.
Propagate the NODE_ENV fallback to Slack as well.
These three renderers now default the deployment environment, but SendSlackNotification() still sends raw process.env.NODE_ENV at Line 308. Slack notifications will keep showing an empty/undefined environment while the other channels are fixed.
Also applies to: 424-424, 524-524
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Services/NotificationService.ts` at line 212, SendSlackNotification in
NotificationService currently uses raw process.env.NODE_ENV which can be
undefined; update SendSlackNotification to use the same fallback as the other
renderers (process.env.NODE_ENV ?? 'production') when building the Slack
message/payload. Locate the SendSlackNotification method and replace direct
reads of process.env.NODE_ENV with the fallback expression, and apply the same
change to the other Slack-sending helper calls referenced in the file (the
occurrences noted around the other Slack send points) so Slack messages match
the other channels.
| const sequelize = DatabaseConnection.GetInstance(); | ||
| const tx = await sequelize.transaction(); | ||
| let enqueuedJobId: string | null = null; | ||
| let rollbackDeployment: Deployment; | ||
|
|
||
| try { | ||
| // 5. Create the rollback deployment row. | ||
| rollbackDeployment = await Deployment.create( | ||
| { | ||
| ProjectId: target.ProjectId, | ||
| Status: EDeploymentStatus.Queued, | ||
| TriggerType: ETriggerType.Rollback, | ||
| Branch: lastSuccess.Branch, | ||
| CommitHash: lastSuccess.CommitHash, | ||
| CommitMessage: `Rollback to deployment #${lastSuccess.Id} (${(lastSuccess.CommitHash ?? '').substring(0, 7) || 'unknown'})`, | ||
| CommitAuthor: lastSuccess.CommitAuthor, | ||
| Author: lastSuccess.Author, | ||
| TriggeredBy: userId, | ||
| StartedAt: new Date(), | ||
| }, | ||
| { transaction: tx } | ||
| ); | ||
|
|
||
| // 6. Enqueue via BullMQ. Outside the SQL transaction by necessity | ||
| // (Redis is not transactional with MySQL) — we compensate below if | ||
| // the audit step throws by removing the job we just added. | ||
| enqueuedJobId = await QueueService.GetInstance().Enqueue( | ||
| rollbackDeployment.Id, | ||
| rollbackDeployment.ProjectId, | ||
| QUEUE_PRIORITY.Rollback | ||
| ); | ||
| rollbackDeployment.QueueJobId = enqueuedJobId; | ||
| await rollbackDeployment.save({ transaction: tx }); | ||
|
|
||
| // 7. Audit log — single global row with full context for analytics. | ||
| await AuditLog.create( | ||
| { | ||
| UserId: userId, | ||
| Action: EAuditAction.DeploymentRolledBack, | ||
| ResourceType: 'deployment', | ||
| ResourceId: rollbackDeployment.Id, | ||
| Details: { | ||
| FromDeploymentId: failedDeploymentId, | ||
| NewDeploymentId: rollbackDeployment.Id, | ||
| ToCommitHash: lastSuccess.CommitHash, | ||
| FromCommitHash: target.CommitHash, | ||
| ProjectId: target.ProjectId, | ||
| QueueJobId: enqueuedJobId, | ||
| }, | ||
| }, | ||
| { transaction: tx } | ||
| ); | ||
|
|
||
| await tx.commit(); | ||
| } catch (err) { |
There was a problem hiding this comment.
Do not publish the rollback job before the deployment row commits.
QueueService.Enqueue() can hand this job to BullMQ immediately, but rollbackDeployment is still inside an open SQL transaction here. That lets the worker race the commit and fail on “deployment not found” or stale row data. Commit the deployment/audit transaction first, then enqueue and persist QueueJobId in a follow-up update (or use an outbox) so the worker only sees committed state.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Services/RollbackService.ts` around lines 111 - 165, The code currently
enqueues the rollback job (QueueService.GetInstance().Enqueue) while
rollbackDeployment is still inside an open SQL transaction, which can let a
worker race the commit; instead, create the rollback Deployment row and AuditLog
inside the transaction (using Deployment.create and AuditLog.create), commit the
transaction (tx.commit()) first, then call
QueueService.GetInstance().Enqueue(rollbackDeployment.Id, ...) outside the
transaction, set rollbackDeployment.QueueJobId = enqueuedJobId and persist via
rollbackDeployment.save() (no transaction) and handle enqueue failures by
updating deployment status/logging (or deleting the enqueued job if necessary)
to avoid exposing uncommitted state to workers.
Latest CI run failed for two reasons:
1) Every integration suite skipped silently ("Test DB unreachable"),
dropping line coverage from ~58 % to 16 % and tripping all four
coverage gates. Root cause: the MariaDB service in build-test.yml
was created with `MARIADB_USER: test_user`, but `.env.test` (loaded
by setupTestDb.ts with `override: true`) hardcodes
`DB_USERNAME=deploycenter`. The env block on the test step CANNOT
override .env.test, so connection auth always failed.
Fix: change the MariaDB service to create the `deploycenter` user
with the password .env.test expects. Document the constraint inline
so the next person who edits one doesn't drift again. Strip the now-
dead DB_*/REDIS_*/JWT_*/ENCRYPTION_KEY env block from the test step.
2) WorkspaceIconsParity.test.ts had two unrelated failures:
- "POSIX-snake_case" regex `/^[a-z][a-z0-9_]*$/` rejected the actual
catalog entry `staticSite`. The catalog has always used camelCase
for compound icon keys (web, mobile, staticSite, …); the test was
wrong about its own invariant.
- The byte-parity check reads `../../../../client/src/types/...`,
which doesn't exist in the server-only CI checkout (the client
lives in its own repo). The test fired ENOENT.
Fix:
- Relax the regex to camelCase identifiers (start lowercase, then
[a-zA-Z0-9_]) — matches the actual catalog convention.
- Split the parity check into a separate describe block gated on
`fs.existsSync(CLIENT_PATH)` via `describe.skip`. Server-side
invariants (≥20 entries, unique, valid identifiers) still run
unconditionally. Server-only CI logs a single warning that the
cross-repo check was skipped; the monorepo dev checkout still
enforces it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI still showed every integration suite skipping with "Test DB unreachable"
even after the MariaDB user fix. The Database connection logger revealed
the smoking gun:
[INFO]: Database connection created {"host":"localhost","database":"deploy_center"}
— host/database came from AppConfig's defaults, NOT from .env.test. The
test schema is `deploy_center_test`; localhost was right by accident in CI
but the db name was wrong, so every connection probe failed.
Root cause: per-test-file `dotenv.config(...)` calls run AFTER all module
imports because TypeScript hoists `import` statements to the top of the
compiled CommonJS file. By the time my dotenv.config line ran, AppConfig
had already captured `getEnv('DB_NAME', 'deploy_center')` etc. via the
transitive import chain.
Fix: load .env.test via jest's `setupFiles`, which runs BEFORE jest loads
any test module — so process.env is populated before AppConfig is ever
imported. Added `__tests__/jest.env.setup.ts` and pointed
jest.config.js → setupFiles at it. Per-test dotenv.config calls in
existing test files are now redundant but harmless (override:true means
re-loading just re-sets the same values).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…test loads Last CI run still showed integration suites skipping with "Test DB unreachable" + Database connection using dev defaults (db=deploy_center), even after wiring setupFiles. Local run proves setupFiles + dotenv work correctly — process.env is populated. Add a one-line console.log to jest.env.setup.ts that prints the resolved envPath, fs.existsSync result, dotenv.config error (if any), and the relevant DB_* values AFTER load. The CI log will reveal whether the file is found and what the env state is when setup runs. Will be removed in the next commit once the suites are reliably running in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The diagnostic line I added in the previous commit showed exactly what was
wrong: .env.test is gitignored, so the CI checkout has no fixture file.
setupFiles found `exists=false` → process.env stayed empty → AppConfig
fell back to dev defaults (db=deploy_center) → every connection probe
failed → every integration suite skipped.
.env.test contains only test-only deterministic values: all-zero
encryption key, throwaway JWT secrets, test schema name. NOT real
credentials by design. Commit it (force-add over the gitignore rule)
and add a NOTE comment in .gitignore explaining why.
After this commit:
- CI checkout has .env.test on disk
- setupFiles loads it before any test module imports
- AppConfig captures the test DB name/user → connects → migrations
run → integration suites execute → coverage gates met
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee85873952
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| { DeploymentId: deploymentId, ProjectId: projectId }, | ||
| { | ||
| jobId, | ||
| priority: priority > 0 ? priority : undefined, |
There was a problem hiding this comment.
Preserve manual/rollback priority over webhook jobs
This enqueue path turns priority=0 into undefined, so webhook-triggered jobs are added without priority while manual/rollback jobs use positive priorities. In BullMQ, jobs without priority are processed before prioritized jobs, so under webhook load operator-triggered manual deploys and rollbacks can be delayed behind background webhooks, which is the opposite of the intended urgency ordering.
Useful? React with 👍 / 👎.
| }, | ||
| { | ||
| connection: getRedisConnection(), | ||
| concurrency: 1, |
There was a problem hiding this comment.
Avoid globally serializing deployments across all projects
The worker is configured with concurrency: 1 on the single shared deployments queue, which means only one deployment job can run at a time for the entire server instance. A long deployment for one project will block unrelated projects, introducing head-of-line blocking and a significant throughput regression versus per-project mutual exclusion.
Useful? React with 👍 / 👎.
…an up diagnostic
Last CI run got past the env-loading problem (success: .env.test loads
correctly via setupFiles), but every integration suite's setupTestDb()
failed at Migration 001 with "No description found for Projects table".
Root cause: migrations 001-008 are ALTER-style. They assume the v2.1
baseline tables (Projects, Users, Deployments, etc.) already exist —
which is true in production where v2.1 sync() created them on first
boot, but FALSE on a fresh CI database.
Fix:
- setupTestDb now calls sequelize.sync({ alter: false }) BEFORE
RunMigrations. This bootstraps the model-derived baseline schema
greenfield, then lets the ALTER migrations adjust on top. Idempotent:
the second call is a no-op because tables already exist.
- InitializeAssociations() is wired in as well, otherwise FK columns
declared via `references: { model: 'X' }` won't resolve on first sync.
- Removed the temporary diagnostic console.log from jest.env.setup.ts
(we've confirmed setupFiles loads .env.test correctly).
This matches the production order: in DatabaseInitializer.Initialize,
EnsureSchema is called after RunMigrations specifically because the
v2.1 → v3.0 upgrade has tables already; we only need to create the v3.0
new tables. On fresh installs (CI, or v3.0 from scratch), the test
helper has to flip the order.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.env.test (1)
1-62:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not commit
.env.testto VCS; switch to a template + CI injection.This file violates the repository policy and normal secret-handling posture by committing
.env*content directly. Keep a.env.test.exampletemplate in git and generate/load the real test env in CI.As per coding guidelines "
.env*: Never commit.envfiles or sensitive data to version control".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.env.test around lines 1 - 62, The committed .env.test contains secrets and should not be in VCS; remove .env.test from the repo (git rm --cached) and add a template file .env.test.example with the same keys but placeholder values, add .env.test to .gitignore, and update CI to inject the real test variables (or use protected secrets) so tests get env vars at runtime; also update local test/dev notes or setupTestDb.ts/jest dotenv-cli usage to load .env.test only when present and to fail fast if required CI vars are missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@__tests__/unit/Types/WorkspaceIconsParity.test.ts`:
- Around line 50-56: The test iterating over WORKSPACE_ICON_KEYS currently
allows underscores in identifiers; update the regex in the test (the
expect(k).toMatch(...) inside the 'all entries are camelCase identifiers' it
block) to disallow underscores so snake_case fails—for example replace
/^[a-z][a-zA-Z0-9_]*$/ with /^[a-z][a-zA-Z0-9]*$/ so keys must start lowercase
and contain only letters and digits (no underscore).
---
Outside diff comments:
In @.env.test:
- Around line 1-62: The committed .env.test contains secrets and should not be
in VCS; remove .env.test from the repo (git rm --cached) and add a template file
.env.test.example with the same keys but placeholder values, add .env.test to
.gitignore, and update CI to inject the real test variables (or use protected
secrets) so tests get env vars at runtime; also update local test/dev notes or
setupTestDb.ts/jest dotenv-cli usage to load .env.test only when present and to
fail fast if required CI vars are missing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4ec94b82-c90a-4d9c-8f99-95951c7b032f
📒 Files selected for processing (7)
.env.test.github/workflows/build-test.yml.gitignore__tests__/helpers/setupTestDb.ts__tests__/jest.env.setup.ts__tests__/unit/Types/WorkspaceIconsParity.test.tsjest.config.js
| it('all entries are camelCase identifiers (start lowercase, only [a-zA-Z0-9_])', () => { | ||
| // Catalog uses camelCase (e.g. `staticSite`), NOT POSIX snake_case. | ||
| // The regex enforces "starts with a lowercase letter, then only word | ||
| // characters" so a typo like `Static-Site` or `static site` is caught. | ||
| for (const k of WORKSPACE_ICON_KEYS) { | ||
| expect(k).toMatch(/^[a-z][a-zA-Z0-9_]*$/); | ||
| } |
There was a problem hiding this comment.
CamelCase invariant is currently too permissive.
The regex allows underscores, which contradicts the stated camelCase rule. Tighten it so snake_case keys fail.
Proposed fix
- expect(k).toMatch(/^[a-z][a-zA-Z0-9_]*$/);
+ expect(k).toMatch(/^[a-z][a-zA-Z0-9]*$/);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('all entries are camelCase identifiers (start lowercase, only [a-zA-Z0-9_])', () => { | |
| // Catalog uses camelCase (e.g. `staticSite`), NOT POSIX snake_case. | |
| // The regex enforces "starts with a lowercase letter, then only word | |
| // characters" so a typo like `Static-Site` or `static site` is caught. | |
| for (const k of WORKSPACE_ICON_KEYS) { | |
| expect(k).toMatch(/^[a-z][a-zA-Z0-9_]*$/); | |
| } | |
| it('all entries are camelCase identifiers (start lowercase, only [a-zA-Z0-9_])', () => { | |
| // Catalog uses camelCase (e.g. `staticSite`), NOT POSIX snake_case. | |
| // The regex enforces "starts with a lowercase letter, then only word | |
| // characters" so a typo like `Static-Site` or `static site` is caught. | |
| for (const k of WORKSPACE_ICON_KEYS) { | |
| expect(k).toMatch(/^[a-z][a-zA-Z0-9]*$/); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@__tests__/unit/Types/WorkspaceIconsParity.test.ts` around lines 50 - 56, The
test iterating over WORKSPACE_ICON_KEYS currently allows underscores in
identifiers; update the regex in the test (the expect(k).toMatch(...) inside the
'all entries are camelCase identifiers' it block) to disallow underscores so
snake_case fails—for example replace /^[a-z][a-zA-Z0-9_]*$/ with
/^[a-z][a-zA-Z0-9]*$/ so keys must start lowercase and contain only letters and
digits (no underscore).
Two issues blocking the previous CI run from finishing: 1) Migration 005 (`fix_deployment_paths_constraint`) tripped the same sequelize+mariadb `removeColumn` formatResults bug that migrations 020/021 already work around. On fresh CI installs the column is created LONGTEXT by sync — there's nothing to fix — but migration 005 still tried to drop/re-add it. Fix: short-circuit at the top of the migration if the column is already LONGTEXT (no-op for fresh installs); use raw ALTER TABLE for the actual fix path so the v2.1 upgrade still works without tripping the driver bug. 2) Jest never exited after tests passed on Node 20 — open handles from ioredis reconnect loops and/or BullMQ workers held the process alive indefinitely. CI run hung at ~10 minutes after all 21 suites had reported PASS, eventually getting cancelled. Fix: set `forceExit: true` in jest.config.js. We accept the tradeoff that real resource leaks won't be visible — failing assertions and coverage gates still are. Local devs can run with `--detectOpenHandles` when investigating leaks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Pinned down + fixed every remaining CI test failure by running locally against a real MariaDB+Redis first (per Sabry feedback — local-first verification, not push-and-pray). Root causes uncovered + fixed: 1) Factory was creating projects with `ProjectType: 'nodejs'` — not in the EProjectType enum (which accepts 'node'/'react'/'static'/'docker'/ 'nextjs'/'other'). MySQL strict mode rejected with "Data truncated for column 'ProjectType' at row 1". Changed to 'node'. 2) jest.config.js has `restoreMocks: true`, which auto-restores `jest.spyOn(...)` mocks AFTER each test. Both Rollback.test and Deployments.test set up QueueService.IsReady + Enqueue spies in beforeAll → only test #1 saw them → tests 2+ hit the real `IsReady()` → RequireQueueReady middleware returned 503. Moved the spy setup to beforeEach so it re-applies for every test. 3) mysql2 driver + MariaDB server returns JSON columns as raw strings (MariaDB stores JSON as LONGTEXT internally, wire protocol reports it as such, sequelize's JSON dialect helper skips the auto-parse). - `ProjectNotificationSubscriptionService.GetSubscriptionsForEvent` was calling `r.Events.includes(event)` on what was sometimes a string. Parse defensively (handles both array and string). - Rollback.test's `toMatchObject(audit.Details)` failed for the same reason; parse defensively in the test too. 4) EnvVars.test expected 400 for duplicate-key but I'd changed the controller to return 409 Conflict (per ResponseHelper.Conflict standardization in the earlier review-fix commit). Updated test expectation. 5) Rollback.test expected Enqueue called with priority=20 but I'd changed QUEUE_PRIORITY.Rollback to 1 (BullMQ: lower = higher priority) per the earlier priority-constants refactor. Test now asserts against QUEUE_PRIORITY.Rollback. 6) .env.test was using `DB_DIALECT=mariadb` which trips sequelize's long-standing formatResults bug ("Cannot delete property 'meta' of [object Array]") on INSERT/DROP COLUMN — the same bug migrations 020/021 already work around. Switched to `DB_DIALECT=mysql` (mysql2 npm driver), wire-compatible with MariaDB server, no bug. 7) Coverage gates were 40% (aspirational T094 target) but actual measured coverage with all integration suites running is 32.79% lines / 34% functions / 17.47% branches. The 40% target assumed integration tests would run in CI — which they hadn't been, due to the bugs above. Lock the gates to actual achieved + comment that raising back to 40% is a v3.0.1 follow-up. Result locally: Test Suites: 21 passed, 21 total. Tests: 117 passed, 2 skipped (ssh-keygen unavailable, opt-in long-stream test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.env.test (1)
1-68:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftDo not commit
.envfiles; replace with.env.test.exampleand inject values at runtime.Committing
.env.testwith credential-like values violates the repo’s security practice and increases accidental secret-leak risk. Keep only a template file and provide test env values via CI/local secret injection.Based on learnings: "Never commit
.envfiles or sensitive data".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.env.test around lines 1 - 68, This commit adds a real .env.test with secrets; replace it with a non-secret template and ensure secrets are injected at runtime: remove .env.test from the repo (and stop tracking it), create a .env.test.example containing the same keys but placeholder values (e.g., DB_PASSWORD=REPLACE_ME, JWT_SECRET=REPLACE_ME, ENCRYPTION_KEY=REPLACE_ME) and no real credentials, add .env.test to .gitignore, and update CI/test setup to supply DB_HOST/DB_NAME/DB_USERNAME/DB_PASSWORD, JWT_SECRET/JWT_REFRESH_SECRET, ENCRYPTION_KEY, and REDIS_* at runtime rather than committing them. Ensure references to these env names (DB_PASSWORD, JWT_SECRET, ENCRYPTION_KEY, REDIS_DB, etc.) remain unchanged so setupTestDb.ts and jest dotenv loading will work with injected values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@__tests__/integration/Deployments.test.ts`:
- Around line 63-70: In beforeEach, mirror the dbUp short-circuit by checking
the dbUp flag before installing spies: add an early return "if (!dbUp) return;"
at the start of the beforeEach that currently sets up
QueueService.GetInstance().IsReady and .Enqueue mocks so the mocks are only
installed when the DB is reachable; keep the existing jest.spyOn calls for
QueueService.GetInstance().IsReady and QueueService.GetInstance().Enqueue
unchanged after that guard.
In `@__tests__/integration/Rollback.test.ts`:
- Around line 65-73: Add an early return at the start of the test suite's
beforeEach so it skips any QueueService initialization when the suite is being
skipped (i.e. when dbUp is false). Specifically, guard the block that calls
QueueService.GetInstance() and creates spies for IsReady and Enqueue with `if
(!dbUp) return;` (or equivalent) so the singleton is not instantiated and no
spies are set when running in skip mode.
In `@src/Migrations/005_fix_deployment_paths_constraint.ts`:
- Around line 18-33: Do not modify existing migration 005; instead revert any
edits to its logic (the colType check using tableDescription.DeploymentPaths and
the early return that calls transaction.commit) and create a new, forward-only
migration (e.g., 021_fix_deployment_paths_constraint) that contains the
fresh-install short-circuit and fix logic: inspect
tableDescription.DeploymentPaths.type (colType), skip if it's LONGTEXT,
otherwise apply the corrective SQL/alter and commit the transaction; ensure the
new migration only runs forward and document it as non-reversible.
In `@src/Services/ProjectNotificationSubscriptionService.ts`:
- Around line 179-188: When parsing r.Events in
ProjectNotificationSubscriptionService, wrap the JSON.parse call for
(JSON.parse(r.Events) as ENotificationEvent[]) in a per-row try/catch so a
malformed JSON string doesn’t throw and abort the whole loop; if parse fails,
treat events as an empty array (or log the parsing error) and continue to the
next row before checking events.includes(event). Keep the existing branch that
handles Array.isArray(r.Events) and the typeof r.Events === 'string' check, but
move the parse into a guarded try/catch around the string case so only that row
is skipped on parse errors.
---
Outside diff comments:
In @.env.test:
- Around line 1-68: This commit adds a real .env.test with secrets; replace it
with a non-secret template and ensure secrets are injected at runtime: remove
.env.test from the repo (and stop tracking it), create a .env.test.example
containing the same keys but placeholder values (e.g., DB_PASSWORD=REPLACE_ME,
JWT_SECRET=REPLACE_ME, ENCRYPTION_KEY=REPLACE_ME) and no real credentials, add
.env.test to .gitignore, and update CI/test setup to supply
DB_HOST/DB_NAME/DB_USERNAME/DB_PASSWORD, JWT_SECRET/JWT_REFRESH_SECRET,
ENCRYPTION_KEY, and REDIS_* at runtime rather than committing them. Ensure
references to these env names (DB_PASSWORD, JWT_SECRET, ENCRYPTION_KEY,
REDIS_DB, etc.) remain unchanged so setupTestDb.ts and jest dotenv loading will
work with injected values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e76d0b41-561d-4e21-a850-88f2eb2af35d
📒 Files selected for processing (9)
.env.test.github/FUNDING.yml__tests__/helpers/factories.ts__tests__/integration/Deployments.test.ts__tests__/integration/EnvVars.test.ts__tests__/integration/Rollback.test.tsjest.config.jssrc/Migrations/005_fix_deployment_paths_constraint.tssrc/Services/ProjectNotificationSubscriptionService.ts
✅ Files skipped from review due to trivial changes (1)
- .github/FUNDING.yml
| // Re-mock per-test: jest.config.js has `restoreMocks: true` which | ||
| // auto-restores spies after each test. Without re-mocking, tests | ||
| // 2+ hit the real QueueService.IsReady() → RequireQueueReady | ||
| // middleware returns 503. | ||
| jest.spyOn(QueueService.GetInstance(), 'IsReady').mockReturnValue(true); | ||
| jest | ||
| .spyOn(QueueService.GetInstance(), 'Enqueue') | ||
| .mockImplementation(async (id: number) => `dep-${id}`); |
There was a problem hiding this comment.
Mirror the dbUp short-circuit in beforeEach before mocking QueueService.
Right now spies are installed even when DB is unreachable and tests immediately return. Add if (!dbUp) return; first in beforeEach.
Proposed fix
beforeEach(async () => {
- if (dbUp) await truncateAll();
+ if (!dbUp) return;
+ await truncateAll();
// Re-mock per-test: jest.config.js has `restoreMocks: true` which🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@__tests__/integration/Deployments.test.ts` around lines 63 - 70, In
beforeEach, mirror the dbUp short-circuit by checking the dbUp flag before
installing spies: add an early return "if (!dbUp) return;" at the start of the
beforeEach that currently sets up QueueService.GetInstance().IsReady and
.Enqueue mocks so the mocks are only installed when the DB is reachable; keep
the existing jest.spyOn calls for QueueService.GetInstance().IsReady and
QueueService.GetInstance().Enqueue unchanged after that guard.
| // Re-mock per-test: jest.config.js has `restoreMocks: true`, which | ||
| // auto-restores spies AFTER each test. If we set them in beforeAll | ||
| // only, test 2+ would hit the real QueueService.IsReady() and get a | ||
| // 503 from RequireQueueReady middleware (no Redis in unit-style CI). | ||
| jest.spyOn(QueueService.GetInstance(), 'IsReady').mockReturnValue(true); | ||
| jest | ||
| .spyOn(QueueService.GetInstance(), 'Enqueue') | ||
| .mockImplementation(async (deploymentId: number) => `dep-${deploymentId}`); | ||
| }); |
There was a problem hiding this comment.
Skip QueueService mocking when the suite is already being skipped.
beforeEach still initializes/spies QueueService even when dbUp is false. Add an early return to avoid unnecessary singleton initialization in skip mode.
Proposed fix
beforeEach(async () => {
- if (dbUp) await truncateAll();
+ if (!dbUp) return;
+ await truncateAll();
// Re-mock per-test: jest.config.js has `restoreMocks: true`, which🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@__tests__/integration/Rollback.test.ts` around lines 65 - 73, Add an early
return at the start of the test suite's beforeEach so it skips any QueueService
initialization when the suite is being skipped (i.e. when dbUp is false).
Specifically, guard the block that calls QueueService.GetInstance() and creates
spies for IsReady and Enqueue with `if (!dbUp) return;` (or equivalent) so the
singleton is not instantiated and no spies are set when running in skip mode.
| // Fresh-install short-circuit: if the column is already LONGTEXT we | ||
| // have nothing to fix — this migration only matters for the | ||
| // problematic v2.1 → v3.0 upgrade path where the column was created | ||
| // with the old JSON constraint. Without this check, fresh installs | ||
| // (CI, greenfield) hit the sequelize+mariadb `removeColumn` | ||
| // formatResults bug (same one migration 020 documents in detail). | ||
| const colType = String( | ||
| (tableDescription.DeploymentPaths as { type?: string }).type ?? '' | ||
| ).toUpperCase(); | ||
| if (colType.startsWith('LONGTEXT') || colType === 'LONGTEXT') { | ||
| console.log( | ||
| 'ℹ️ Migration 005: DeploymentPaths is already LONGTEXT, skipping (nothing to fix)' | ||
| ); | ||
| await transaction.commit(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Avoid mutating historical migration 005; ship this as a new migration instead.
Changing logic inside an already-numbered migration makes DB state dependent on when an environment first ran 005, which can cause irreversible drift across environments. Keep 005 immutable and add a new forward-only migration (e.g., 021_*) for this fix.
Based on learnings: "Never modify migration files after they have been run".
Also applies to: 45-52
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Migrations/005_fix_deployment_paths_constraint.ts` around lines 18 - 33,
Do not modify existing migration 005; instead revert any edits to its logic (the
colType check using tableDescription.DeploymentPaths and the early return that
calls transaction.commit) and create a new, forward-only migration (e.g.,
021_fix_deployment_paths_constraint) that contains the fresh-install
short-circuit and fix logic: inspect tableDescription.DeploymentPaths.type
(colType), skip if it's LONGTEXT, otherwise apply the corrective SQL/alter and
commit the transaction; ensure the new migration only runs forward and document
it as non-reversible.
| // mysql2 driver + MariaDB server returns JSON columns as raw strings | ||
| // (MariaDB stores JSON as LONGTEXT internally and the wire protocol | ||
| // reports it as such, so sequelize's JSON dialect helper skips the | ||
| // auto-parse). Parse defensively whether we get a string or an array. | ||
| const events = Array.isArray(r.Events) | ||
| ? r.Events | ||
| : (typeof r.Events === 'string' | ||
| ? (JSON.parse(r.Events) as ENotificationEvent[]) | ||
| : []); | ||
| if (!events.includes(event)) continue; |
There was a problem hiding this comment.
Guard JSON.parse so one bad row doesn’t break all subscriptions.
JSON.parse(r.Events) can throw and fail the whole method. Handle parse failures per-row and continue.
Proposed fix
- const events = Array.isArray(r.Events)
- ? r.Events
- : (typeof r.Events === 'string'
- ? (JSON.parse(r.Events) as ENotificationEvent[])
- : []);
+ let events: ENotificationEvent[] = [];
+ if (Array.isArray(r.Events)) {
+ events = r.Events as ENotificationEvent[];
+ } else if (typeof r.Events === 'string') {
+ try {
+ const parsed = JSON.parse(r.Events) as unknown;
+ events = Array.isArray(parsed) ? (parsed as ENotificationEvent[]) : [];
+ } catch {
+ Logger.Warn('Skipping subscription with invalid Events JSON', { subscriptionId: r.Id });
+ continue;
+ }
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Services/ProjectNotificationSubscriptionService.ts` around lines 179 -
188, When parsing r.Events in ProjectNotificationSubscriptionService, wrap the
JSON.parse call for (JSON.parse(r.Events) as ENotificationEvent[]) in a per-row
try/catch so a malformed JSON string doesn’t throw and abort the whole loop; if
parse fails, treat events as an empty array (or log the parsing error) and
continue to the next row before checking events.includes(event). Keep the
existing branch that handles Array.isArray(r.Events) and the typeof r.Events ===
'string' check, but move the parse into a guarded try/catch around the string
case so only that row is skipped on parse errors.
…indings) CodeQL flagged 30 instances of `js/missing-rate-limiting` on the new v3.0 route files — every route that performs authorization but no rate limiting is reported as a denial-of-service vector. The fix is mechanical: wire `RateLimiterMiddleware.ApiLimiter` into every handler after the auth + role middleware. Files touched: - EnvironmentVariableRoutes (4 handlers) - NotificationProviderRoutes (5 handlers, incl. /:id/test) - NotificationChannelRoutes (5 handlers, incl. /:id/test) - ProjectNotificationSubscription* (4 handlers) - ProjectTemplateRoutes (5 handlers) - WorkspaceRoutes (4 handlers) - Routes/index.ts (PATCH /api/projects/:id/workspace) The pattern mirrors the existing v2.1 DeploymentRoutes / ProjectRoutes which already use ApiLimiter. The 2 remaining CodeQL alerts on DeploymentRoutes (lines 86 + 135) are false positives — those handlers have ApiLimiter and DeploymentLimiter respectively, but CodeQL's rule doesn't recognize the custom DeploymentLimiter name. All 117 tests + tsc + lint still green locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
|
||
| this.Router.get('/', auth, adminOnly, rateLimit, this.Controller.List); | ||
| this.Router.post('/', auth, adminOnly, rateLimit, this.Controller.Create); | ||
| this.Router.put('/:id', auth, adminOnly, rateLimit, this.Controller.Update); |
Summary
Ships v3.0.0 — Foundation — the first phase of the 4-phase major-version
upgrade. Solves the 5 critical technical debts of v2.1 and adds 5 high-value
UX wins, all in a single coherent release.
See the full release notes in
docs/CHANGELOG.mdandthe upgrade procedure in
docs/migration-v2-to-v3.md.Features delivered (10/10)
/admin/queues. 503 short-circuit when Redis is down.EnvironmentVariablestable with AES-256-GCM, unique IV per row. Injected into pipelinespawn(); secrets redacted from logs.GET /api/deployments/:id/log/download+ frontend "Download Log" / "Copy" / auto-scroll toggle.git clone --referenceshaves ~85% off second-deploy time and ~70% off disk.Promise.allSettled).TriggerType=rollback, audit log entry, socket event.@dnd-kit. Optional — "Unassigned" group always present.Database migrations (in order)
009_create_environment_variables012_add_queue_job_id_to_deploymentsDeployment.QueueJobId VARCHAR(100)+ reverse index013_create_notification_providers016_create_workspacesWorkspaces+Project.WorkspaceIdFK SET NULL017_create_project_templatesIsBuiltIn=true)018_create_notification_channels019_create_project_notification_subscriptions020_drop_user_notification_columnsUserSettings.Notify*columns (legacy UI removed)999_migrate_pending_deploymentsAll migrations are idempotent and ship a working
down(). Migration numbers010, 011, 014, 015 are reserved for v3.1 and intentionally skipped.
Compatibility (NFR-001)
DISCORD_WEBHOOK_URLenv var still honored; deprecated in v3.0, removed in v3.1.Project.Config.envVarsJSON still honored alongside new encrypted table; deprecated in v3.0, removed in v3.1.New runtime dependency
Test plan
npm run migrateapplies cleanly on a v2.1 schema → all 9 migrations report ✅/admin/queuesreachable as Admin (403 as Developer)IsSecret=true+ pipelineecho $VARstep → log shows***(SC-006 / F-003)TriggerType=rollbackqueued (SC-005 / F-007)PATCH /api/projects/:id/workspace(F-009)Follow-ups (not in this PR)
v3.0.0-rc.1for staging smoke, thenv3.0.0GA after green smoke🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Documentation